4 ms·
It's not difficult to write imperative code in Clojure; [1] is an example. Immutability-by-default makes you work to mutate things, for sure, but that in itself
by davidrupp 5y ago
It's not difficult to write imperative code in Clojure; [1] is an example. Immutability-by-default makes you work to mutate things, for sure, but that in itself doesn't make the language inherently functional.
[1] https://clojuredocs.org/clojure.core/let#example-542692c7c026201cdc3269c2 https://clojuredocs.org/clojure.core/let#example-542692c7c02...
- robthethird 5y agoYour link might be pointing to the wrong example. I assume you wanted to show an example of an atom.
- davidrupp 5y agoNope. The sequence of statements in the (let) block is imperative. Atoms are actually an example of how the values of references are updated functionally. Clojure has facilities for both imperative and functional style.
- didibus 5y agoLexical shadowing isn't an imperative thing. Imagine the form: (let [a 10 a (+ a a) a (+ a a a)] a) as being like so: (let [a 10] (let [a (+ a a)] (let [a (+ a a a)] a))) Which you can now re-imagine as the functional composition: (+ (+ 10 10) (+ 10 10) (+ 10 10)) So it has evaluation semantics which allow you to reduce it as described in lambda calculus using α-conversion and β-reduction. And this is different to the imperative style, because in imperative you can do: int a = 10; a = ++a + ++a; Where `a` is now equal to 23, but in Clojure: (let [a 10 a (+ (inc a) (inc a))] a) You now have `a` equal to 22 instead. This is the difference between the variable substitution evaluation semantic of lambda calculus and the imperative semantics.
- davidrupp 5y agoNice strawman. Now, how would you restructure the let bindings (without the println) at https://clojuredocs.org/clojure.core/let#example-542692c7c026201cdc3269c2 https://clojuredocs.org/clojure.core/let#example-542692c7c02... equivalently? Wouldn't the ordering of the function executions matter? Could you re-order the bindings arbitrarily and get the same result? I don't think you can. That makes it imperative-as-opposed-to-declarative, even though it may not be imperative-as-a-synonym-for-side-effecting.
- didibus 5y agoYou do the same thing: (let [a (take 5 (range))] (let [{:keys [b c d] :or {d 10 b 20 c 30}} {:c 50 :d 100}] (let [[e f g & h] ["a" "b" "c" "d" "e"]] (let [_ (println "I was here!")] (let [foo 12] (let [bar (+ foo 100)] [a b c d e f g h foo bar])))))) And now you can simply evaluate it using variable substitution again. The difference is that when you say `(let [a 10])` the variable `a` is a constant now, you can effectively replace all occurrence of it within the scope by `10`, and you can do this at compile time. That's why people say the symbol `a` is bound to the value 10, and not the variable `a` points to the value 10. Effectively you cannot change `a` within the scope anymore, all use of `a` in that scope will be equal, and so within that scope you can change their ordering freely. What I think is confusing you here is that `let` gives you syntax sugar so all the scopes are flattened, but each new binding pair is actually inside a nested scope, and so `a` is still immutable. In this case, it matters almost never, but as I showed, it still can, because in an imperative language you could still try to mutate the variable even in the same expression, and that is impossible to do in Clojure.
- davidrupp 5y ago> each new binding pair is actually inside a nested scope Can you point me to something that demonstrates this? I've both decompiled this form and traced its compilation: (defn foo [a] (let [b a b (+ b a)] b)) and it does create two different symbols with a name of "b", which I get has the same net effect of not mutating the original "b", but I would not consider that "nested scope". It feels more like SSA to me, which is typically quite sequential in nature, which is what I associate with the term "imperative".
- davidrupp 5y agoAlso, I don't think your (incorrect / incomplete) transliteration of the C code example to Clojure demonstrates anything we don't already know; namely that `(inc a)` in Clojure has different semantics from `++a` in C. Of course the Clojure expression you wrote has a different semantic interpretation from the C expression you wrote; they're different expressions. The fact that `++a` is side-effecting doesn't make Clojure code that depends on sequencing not imperative (imperative-as-opposed-to-declarative, that is).
- didibus 5y agoThe difference is that it is not possible to modify the value of `a` in the scope that it is bound too. Try as you want, but you won't be able to write an `inc` that modifies the value of `a` within that scope. The declarative nature is that you're declaring that you want `a` to mean 10 within some scope. That's why you say "let `a` be 10 in current scope", that's your intention here, for `a` to be 10. After you've declared that, it holds true no matter what. Where as in the imperative style you say: put 10 at place `a`. This is variable assignment, there's a place which is refered too as `a` and inside that place you can set values and change them at will. At any point you can instruct the language to change what value is at place `a`, there are no restrictions to the instructions you can give. It's a bit of a oversimplification to claim that imperative programming is distinguished from declarative programming by assuming a lack of ordering in the latter. Functional programming is not prevented from expressing order, rather it is less able to express random accidental order at the operational semantics level. At the end of the day, it's very much about the computational model, imperative is based on Turing model, and Functional on the lambda calculus. Because the latter doesn't depend on a global mutable running state, it is said to be declarative, in the sense that what you see is what you get, you don't need to keep track of what the memory currently has to proceed to the next step. That said, you're right that Clojure supports some forms of imperative programming as well, but let isn't one, your parent commenter pointing to Atom was more on point. That's what you'd need to do to get back a more imperative `let`: (let [a (atom 10) a (+ (swap! a inc) (swap! a inc))] a) Will now give you 23.
- weavejester 5y ago"The sequence of statements in the (let) block is imperative." The println in the example is imperative, as it has side-effects, but a let block having an ordering of bindings is not inherently imperative. You can replace any let block with a set of nested functions: (let [foo 12 bar (+ foo 100)] [foo bar]) ((fn [foo] ((fn [bar] [foo bar]) (+ foo 100))) 12) Sequential ordering of 'statements' is not automatically imperative. On the other hand, atoms are imperative, as they are mutable and therefore side-effectful.
- davidrupp 5y agoHmm. I usually think of "imperative" as "as opposed to declarative", and sequential ordering is one of its hallmarks in that context. And for Clojure atoms specifically I think of "functional" as in Okasaki's "purely functional data structures". Language is hard.