5 ms·
What I heard from colleagues that work with Clojure is that it is a horrible language where the default way of writing code is an imperative programming style w
by AtNightWeCode 5y ago
What I heard from colleagues that work with Clojure is that it is a horrible language where the default way of writing code is an imperative programming style where contexts are passed around and updated. Far from the concepts of functional programming.
- stingraycharles 5y agoI’m not sure I understand what you mean. I don’t consider Clojure to be imperative at all — everything is immutable by default, and you actually have to go through considerable efforts to write things in an imperative style. When I compare Clojure to another functional language I know well, Haskell, one of the things I really feel it lacks is proper pattern matching and currying; yes there are libraries that you can use, but it’s just not idiomatic to do in Clojure. I would not, however, assert that it’s an imperative language. Could you care to elaborate on what exactly you find is lacking in Clojure?
- davidrupp 5y agoIt'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.
- divs1210 5y agoWhat is this? Are you being serious? I've worked in multiple Clojure shops and it has always been amazing. All the core code and business logic etc. (kernel) is implemented functionally, and the interface with the outside world (shell) is implemented imperatively. Clojure being a horrible language is a really hot take and it being based on hearsay makes your coomment look like it's not in good faith.
- AtNightWeCode 5y agoAs with all functional programming languages, if you limit the use to some specific areas you can get a lot done with a few easily understood lines of code.
- StreamBright 5y agoQuite the opposite. Functional style is especially useful in larger codebases. However, I think a functional strongly typed language is often easier to get right than a weakly typed one. I mostly write F# and Clojure and based on my experience I would go for F# any day over Clojure but at the same time I would also go for Clojure over Java as well. I do not know where your views are coming from, sounds like 2nd hand experience rather than 1st one.
- deleted 5y ago[deleted]
- AtNightWeCode 5y agoAs I stated in the beginning I do not work with Clojure. Pure functions is something I use in any programming language. I have a hands on experience from a lot of different functional languages though including F#. From what I understand it is very common to use context based style of coding in Clojure. That is at least what I heard and Google does not seem to disagree...
- xpe 5y agoPlease be specific with your links and sources.
- ltultraweight 5y agoI use Clojure quite a bit and I don't write anything imperatively. But passing contexts I can see. There is a not uncommon pattern that one can use of keeping a large map with state in it. However it's completely compatible with pure functional programming.
- raspasov 5y agoAt first I thought you comment was sarcasm but it does appear to be serious. There's nothing "imperative" (as commonly understood) in 98% of Clojure code out in the wild. "Contexts" or "context updates" are also very rare, unless required by a non-Clojure JVM or JS library. Can discuss further if you reference a specific open-source code example with "context" or "imperative style".
- didibus 5y agoWell it's a bit preposterous of you to say that without actually having tried Clojure in good faith. But, since we all make opinions from others, let me provide you a balance of opinions by giving you mine. I'm a senior engineer and I currently use Clojure professionally. I find it to be a lovely, fun and productive language, my favorite one to date actually. I have prior professional experience with C++, C#, ActionScript 3, JavaScript, Scala, Kotlin and Java. And of all of those, Clojure is my favourite to work with. The "context' pattern you may have heard of, I believe I know what it refers too, and it is not an imperative pattern at all, let me explain. There's often a case where a piece of functionality will be implemented by multiple functions composed together. For example, an API handling a request will delegate to sub-functions which could in turn call down to more sub-functions. In that scenario, it can happen that the sub-functions need data about the context of the call to the parent function, or they need data generated by the prior sub-functions that were called. In my example, lets say the request object to the API dictates various options and has the request details, and each options is relevant to different sub-functions, maybe one needs the username, where the other needs the cart-id both of which were passed on the request, but maybe the other function also needs the user-permissions which was obtained from the prior sub-function. Ok, so say: someAPI({username: "foo", cart-id: 123}) { permissions = get-permissions(username); retrieve-cart-items(username, permissions); } Now as you have more and more of these forming a coherent whole, in Clojure there is a pattern where people will say, all this initial data and the data generated by the intermediate steps becomes the "context" data for the operation as a whole. (defn some-api [context] (-> get-permissions retrieve-cart-items :cart-items)) Where context is at first a map of the request data: {:username "foo", :cart-id 123} And after the call to "get-permissions" it is a map of the initial request data and the intermediate data added by get-permissions: {:username "foo", :cart-id 123, :permissions [:can-view-cart]} And after the call to retrieve-cart-items it now also contains the cart-items: {:username "foo", :cart-id 123, :permissions [:can-view-cart] :cart-items [:pen, :paper]} Thus all implementing functions for the some-api operations are designed so they take an initial context map and return a context map with added context. And generally in Clojure you'd use the destructuring syntax to indicate what in the context map you depend on: (defn get-permissions [{:keys [username]:as context}] ...) So you see that get-permissions expect the :username key to be present on the context. But, this is still fully functional, because none of the functions share a mutable reference to a shared context, they take an immutable context as input and return a new immutable context as output which will just happen to also include all the key/values of the context they received. Sometimes people also pass in dependencies using this context pattern, which is a form of dependency injection through parametrization, but you combine all dependencies into one parameter object.