Clojure's Deadly Sin

xylon3 pts0 comments

Clojure's deadly sin - Clojure Goes Fast

↰ Back to list

Jul 27, 2023<br>Clojure's deadly sin

This article is about laziness in Clojure. It is intended to be a comprehensive<br>and objective (however possible) critique of lazy sequences as a feature. In<br>no way do I want this to be a judgment of the decision to make Clojure lazy.<br>Clojure the language is by no means formulaic; creating it involved making a<br>plethora of impactful choices. We can judge by Clojure's longevity that the<br>total package has been a success, but it's only natural that some decisions<br>proved to be more accurate than others. Finally, this is not meant to criticize<br>the people behind Clojure. Hindsight doesn't need glasses; it is incredibly<br>tough to write a language (let alone a successful one) and easy to pick on its<br>perceived shortcomings. Yet, I'm still willing to do the latter because I<br>believe my diatribe can be helpful.<br>My goal is to align the Clojure community's stance on laziness. Times and times<br>again, the developers find themselves grappling with the complexities of the<br>lazy approach, attributing their struggles to the lack of understanding of the<br>Clojure Way[1]. I want to prove that many things make<br>up the Clojure Way, and laziness doesn't have to be a defining characteristic. I<br>want programmers to reduce or eliminate their reliance on laziness and not feel<br>guilty about it just because laziness has been deeply ingrained in Clojure since<br>its inception.<br>Only time will tell if writing this is beneficial, but I'm willing to try. Also,<br>sorry about the clickbait<br>title, it was too juicy to<br>pass up.<br>What is laziness?<br>Lazy evaluation, also known as deferred execution, is a programming language<br>feature that delays producing the result of computation until the value is<br>explicitly needed by some other evaluation. There are several different<br>perspectives on how one can look at it, they are all similar, but each can give<br>you some fresh insight:<br>Separation of declaration and execution in time. Writing an expression<br>does not immediately produce the result. There are now two stages of<br>computing the value that a programmer has to be aware of (whether it's good<br>or bad will be discussed later).<br>Declarative vs imperative . The separation above encourages the developer<br>to think about the program more in declarative terms. A program is not a<br>line-by-line instruction for the computer to execute but more of a flexible<br>recipe. This makes programming a bit closer to math, where writing a formula<br>on paper does not immediately force you to solve it (and make your brain<br>hurt). For the compiler, the declarative approach enables additional<br>optimizations because it is free to reorder or even eliminate some steps of<br>the execution.<br>Program as a tree of evaluations. With all values being lazy and<br>dependent upon one another, the whole program becomes this declarative tree<br>just waiting to be executed. Tap the root — the entrypoint — and the tree<br>will recursively walk itself, compute from the leaves downwards[2], and collapse into a single result. Leaves and branches that<br>are not connected to the root are left unevaluated.<br>Pull vs push. If you like this analogy more, lazy evaluation is a pull<br>approach. Nothing gets produced until it is explicitly called for (pulled).<br>In pervasively lazy languages like Haskell, every expression produces a lazy<br>value. It won't even compute 2 + 3 for you unless something else needs it. By<br>invoking the "program as a tree" reasoning, it becomes apparent that "something<br>else" ultimately has to affect the outer world somehow, to have a side<br>effect — print to screen, write to a file, etc. Without side effects, a lazy<br>program is a festive cookbook recipe you never bake.<br>The principles of lazy evaluation are easy to simulate in any language that<br>supports wrapping arbitrary code into a block and giving it a name (anonymous<br>functions, anonymous classes — they all work). In Clojure, that would be a plain<br>lambda or a dedicated delay construct:<br>(fn [] (+ 2 3)) ;; As lazy as it gets

(delay (+ 2 3)) ;; Similar, but the result is computed once and cached.<br>What distinguishes laziness as a language feature rather than a technique is<br>that it occurs automatically and transparently for the user. The code that<br>consumes a value doesn't have to know whether the value is lazy or not, the API<br>is exactly the same, and there is no way to tell (actually, there sometimes is,<br>but it's rarely necessary). In contrast, a delay also represents a deferred<br>computation, but it has to be explicitly dereferenced with a @.<br>Laziness in Clojure<br>While Clojure was inspired by Haskell in multiple ways, its approach to laziness<br>is much more pragmatic. Laziness in Clojure is limited only to lazy<br>sequences . Note that we don't say "lazy collections" because a sequence is the<br>only collection that is lazy. For example, updating a hashmap is eager in<br>Clojure, while in Haskell, it would be lazy:<br>(assoc m :foo "bar") ;; Happens immediately<br>There are several sources whence a developer can...

clojure lazy laziness program language value

Related Articles