News for hackers. Stuff that matters.
Re:Tour of our 250k line Clojure codebase by mping on Thursday June 03, 2021 @03:17PM (#27386056)
This is what I imagine experienced clojure developers can squeeze out of a language like clojure. I would venture that they can train a junior programmer in a couple of weeks, and make them productive very fast.

I guess they could make it work in any language but judging by the description clojure is indeed a great fit, due to the macro capabilities, flexibility and solid runtime via JVM.

Re:Tour of our 250k line Clojure codebase by tmountain on Thursday June 03, 2021 @03:38PM (#27386288)
Yeah, it's nice to see such thoughtful adoption of language facilities clearly oriented towards creating a successful and maintainable codebase.

The Clojure team has always done a nice job expressing the rationale for specific language features, and these rationales often lean towards solving problems that system designers historically faced.

Oftentimes, I think folks think of modern languages in regard to their syntax, tooling, ergonomics, etc; however, to me, the more interesting benefit in adopting a modern language is in how its inbuilt features address design problems that earlier generation languages exposed.

For a real world example of what I'm talking about, you can google "clojure expression problem" and find compelling articles about how Clojure solves this with protocols.

Providing a toolkit for attacking categories of problems inherently gets people focused on the fact that these problems exist in the first place when they may not even recognize them otherwise, and regardless of the choice of language, it leads to better design oriented thinking in the context of larger and more complex systems.

Re:Tour of our 250k line Clojure codebase by doyougnu on Thursday June 03, 2021 @03:56PM (#27386467)
Clojure doesn't solve the expression problem and the expression problem tends to be trivial in dynamically typed languages.

Strictly speaking the expression problem is only defined for _static_ type systems because its concerned with _static_ type safety and extensions without recompilation.

Re:Tour of our 250k line Clojure codebase by tmountain on Thursday June 03, 2021 @05:29PM (#27387587)
It does solve it and doing so was a key design goal of protocols. It's right there in the docs:

There are several motivations for protocols:

Avoid the 'expression problem' by allowing independent extension of the set of types, protocols, and implementations of protocols on types, by different parties

I agree that it's a concern in static type systems, but the same issue rears its head when defining methods which operate on specific types of strongly typed data, so no, it's not always trivial, nor is it a problem exclusive to statically typed languages, and if it was, there wouldn't be a need to introduce a new abstraction for which addressing the problem is a key goal.

Re:Tour of our 250k line Clojure codebase by trutannus on Thursday June 03, 2021 @03:27PM (#27386168)
> would venture that they can train a junior programmer in a couple of weeks, and make them productive very fast

I've got some experience with functional languages, and a good amount with functional features in OO languages. Started learning Clojure this week, and was pleasantly surprised with how quickly I could get working projects up and running. Less upfront learning curve than Elixir, which was unexpected.

Re:Tour of our 250k line Clojure codebase by cliftonk on Thursday June 03, 2021 @03:22PM (#27386106)
I've found I typically reach for clojure when i need to do something on the jvm and want a better java than java.
Re:Tour of our 250k line Clojure codebase by dustingetz on Thursday June 03, 2021 @03:59PM (#27386518)
Clojure's value prop is:

1) default immutability (same simple data structures used in every library -> ecosystem composes better)

2) portable code across multiple host platforms (jvm, node, browser)

3) metaprogramming

IMO, in 2007, immutability on the JVM was a competitive value prop, but in 2021+ it is nothing special. It is the combination of the three things which is a competitive value prop today. Metaprogramming in particular is a mostly unexplored frontier, because it is very hard to do well, and very easy to do badly. Default immutability is kind of a necessary starting point to do metaprogramming well.

Re:Tour of our 250k line Clojure codebase by davidrupp on Thursday June 03, 2021 @04:45PM (#27387050)
> in 2007, immutability on the JVM was a competitive value prop, but in 2021+ it is nothing special

Can you elaborate on this? What do you think has changed in that time to make it "nothing special"?

Re:Tour of our 250k line Clojure codebase by dustingetz on Thursday June 03, 2021 @05:05PM (#27387308)
immutability as a library is available in basically all mainstream languages now and mainstream frameworks leverage it (react, any UI framework, spark, any data framework or database); JS vms are competitive with the JVM and the JVM might even be losing ground in the cloud; typescript is a monster and is letting people explore haskell concepts in industry applications; scala is way better in 2021 than it was in 2011; every new PL can compile to JS and supports immutability. Clojure's sweet spot is currently developing sub-languages embedded in Clojure like RPL is doing (we are doing exactly the same thing at hyperfiddle). That is still too hard to do in typescript imo. Maybe it is possible in scala 3 but it took them 10 years of pure autism to figure out how to do monadic IO in scala due to how complex scala is, so i'd expect it to take another 10 years to figure out how to do metaprogramming in a commercially viable way, I'd be happy to be proven wrong.
Re:Tour of our 250k line Clojure codebase by davidrupp on Thursday June 03, 2021 @05:10PM (#27387361)
"10 years of pure autism" -- This made me smile.
Re:Tour of our 250k line Clojure codebase by cutler on Friday June 04, 2021 @08:03AM (#27392753)
As far as the JVM goes only Clojure and Scala have real immutability built-in, ie. persistent data structures. Kotlin's default immutability is more like read-only.
Re:Tour of our 250k line Clojure codebase by bcrosby95 on Thursday June 03, 2021 @11:30PM (#27390255)
I think Clojure is still a great choice when your concurrent solution to a problem is amenable to working with stale data. If it is then the correctness of your design will rely heavily upon immutability, and the bugs that can be introduced by not using an immutable-first language like Clojure just isn't worth the risk.
Re:Tour of our 250k line Clojure codebase by forgetfulness on Thursday June 03, 2021 @03:34PM (#27386241)
I think that's also why Clojure use peaked right before Java 8 was released; once Java became a better Java, and libraries started to be designed around more ergonomic APIs than wiring up objects that needed miles of stateful configuration code, the pressure that drove you out of Java and into Clojure began to diminish.
Re:Tour of our 250k line Clojure codebase by cutler on Friday June 04, 2021 @08:08AM (#27392778)
Hardly. Despite being Java-compatible Clojure's philosophy is diametrically opposed to that of Java/OOP so anyone who has experienced the benefits of Clojure would hardly return to Java simply because it managed to fake FP with Single Abstract Methods.
Re:Tour of our 250k line Clojure codebase by forgetfulness on Friday June 04, 2021 @08:18PM (#27400702)
Well, maybe there were not as many die-hard lispers within the Clojure community and many were looking for something that handled better than Java 7. I know two other former Clojure programmers that went to Scala, but also find Java passable enough to just use it rather than Clojure.
Re:Tour of our 250k line Clojure codebase by lukashrb on Thursday June 03, 2021 @03:30PM (#27386194)
Interesting take since Clojure and Java are two very different languages. And unlike for example Kotlin, Clojure does not try to be a better Java than Java. But true though Clojure leverages the power of the jvm.
Re:Tour of our 250k line Clojure codebase by forgetfulness on Thursday June 03, 2021 @03:43PM (#27386344)
It was a better Java in many ways.

The doto macro was a relief back in the days when Java APIs insisted on being designed around stateful setters and getters rather than the builder pattern, it allowed you to operate on such unfortunately designed objects in single logical blocks and simulating it all being an expression.

Proxy allowed you to instantiate anonymous inner classes implementing only the methods of an interface that you needed, the rest you could omit; in Java you have to put them all, empty, which necessitated that you use an IDE to generate them, and back then IDEs were not as nice as today.

Those two alone made interacting with contemporary Java libraries so much easier.

It also was convenient to go from Java collections to Clojure collections and vice versa.

Re:Tour of our 250k line Clojure codebase by pjmlp on Thursday June 03, 2021 @04:08PM (#27386633)
" We were not out to win over the Lisp programmers; we were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp."

-- Guy Steele

Re:Tour of our 250k line Clojure codebase by __jem on Thursday June 03, 2021 @04:08PM (#27386641)
Interop from Clojure -> Java is still incredibly easy, though. I often will just wrap a Java library rather than look for a pre-existing Clojure implementation because it's so easy. Of course, most Java libs don't value immutability and functional patterns, but you can still push this kind of interop to the edges of your system and keep everything else in pure Clojure.
Re:Tour of our 250k line Clojure codebase by StreamBright on Thursday June 03, 2021 @05:29PM (#27387591)
Depends on the style the Java library you are trying to use is written in. I have had mixed results. Especially the Java 8 functional style had some challenges. I might have the relevant SO question somewhere.
Re:Tour of our 250k line Clojure codebase by pjmlp on Thursday June 03, 2021 @04:06PM (#27386615)
Basically you get a new version of a Lisp Machine, that you can sneak up in lots of places, whereas with Common Lisp it isn't so easy to do so.
Re:Tour of our 250k line Clojure codebase by AtNightWeCode on Thursday June 03, 2021 @04:49PM (#27387096)
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.
Re:Tour of our 250k line Clojure codebase by divs1210 on Thursday June 03, 2021 @05:03PM (#27387274)
What 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.

Re:Tour of our 250k line Clojure codebase by AtNightWeCode on Thursday June 03, 2021 @05:27PM (#27387569)
As 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.
Re:Tour of our 250k line Clojure codebase by StreamBright on Thursday June 03, 2021 @05:35PM (#27387646)
Quite 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.

Re:Tour of our 250k line Clojure codebase by AtNightWeCode on Thursday June 03, 2021 @06:05PM (#27388013)
As 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...
Re:Tour of our 250k line Clojure codebase by xpe on Thursday June 03, 2021 @08:59PM (#27389286)
Please be specific with your links and sources.
Re:Tour of our 250k line Clojure codebase by stingraycharles on Thursday June 03, 2021 @04:56PM (#27387188)
I’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?

Re:Tour of our 250k line Clojure codebase by davidrupp on Thursday June 03, 2021 @05:03PM (#27387283)
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-542692c7c02...

Re:Tour of our 250k line Clojure codebase by robthethird on Thursday June 03, 2021 @06:34PM (#27388295)
Your link might be pointing to the wrong example. I assume you wanted to show an example of an atom.
Re:Tour of our 250k line Clojure codebase by davidrupp on Thursday June 03, 2021 @08:16PM (#27389015)
Nope. 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.
Re:Tour of our 250k line Clojure codebase by didibus on Thursday June 03, 2021 @11:36PM (#27390290)
Lexical 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.

Re:Tour of our 250k line Clojure codebase by davidrupp on Friday June 04, 2021 @10:39AM (#27394066)
Nice strawman. Now, how would you restructure the let bindings (without the println) at 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.
Re:Tour of our 250k line Clojure codebase by didibus on Friday June 04, 2021 @05:12PM (#27399131)
You 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.

Re:Tour of our 250k line Clojure codebase by davidrupp on Sunday June 06, 2021 @01:00PM (#27414476)
> 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".
Re:Tour of our 250k line Clojure codebase by didibus on Sunday June 06, 2021 @05:54PM (#27416710)
Ok, came up with another example. See how let:

    (let [a 1
          a (+ (inc a) (inc a))
          _ (println a)
          a (+ a a)]
     a)
Can be rewritten simply as a nested composition of anonymous functions:

    ((fn[a]
      ((fn[a]
        ((fn[_]  
          ((fn[a]
            a)))
         (println a))
        (+ a a))
       (+ (inc a) (inc a))))  
     1)
The latter representation you would consider functional programming no? Well the let is just syntactic sugar for it and can be rewritten in terms of only composed anonymous functions (aka lambdas).

And like I said previously, you can reduce it with variable substitution, which gets rid completely of all the variables, at compile time, and the evaluation will give the same result:

    (do
     (println (+ (inc 1) (inc 1)))
     (+ (+ (inc 1) (inc 1))
        (+ (inc 1) (inc 1))))
From the let form, or from its corresponding anonymous function form:

    ((fn[_]  
      ((fn[]
        (+ (+ (inc 1) (inc 1))
           (+ (inc 1) (inc 1))))))        
     (println (+ (inc 1) (inc 1))))
In this case you see more clearly that the side-effect causes impurity, since it can't really be reduced, it's only in this case that order dependence matters, and so we can't eliminate the wrapping function, and this relies purely on Clojure's left to right argument evaluation ordering which allows you to mix/match side-effects within pure functions with predictable effect timing.

So here what happens you reduce that function into a do-block (which is Clojure's imperative form):

    (do
     (println (+ (inc 1) (inc 1)))
     ((fn[]
       (+ (+ (inc 1) (inc 1))
          (+ (inc 1) (inc 1)))))) 
And now you can further reduce the pure parts, which takes us back to what we had when we reduced the let:

    (do
     (println (+ (inc 1) (inc 1)))
     (+ (+ (inc 1) (inc 1))
        (+ (inc 1) (inc 1))))
And finally this can be reduced further:

    (do
     (println (+ 2 2))
     (+ (+ 2 2)
        (+ 2 2)))

    (do
     (println 4)
     (+ 4
        4))
To our most reducible form:

    (do
     (println 4)
     8)
This reduction can all happen in parallel or in any order, and the result will always be the same.

Ok, and this last bit is very important, this is what people mean when they say that in functional programming the order of execution doesn't matter. The side-effects must still be sequenced in their correct order, but all the computation can happen in arbitrary order, because the computation doesn't rely on a sequence of instructions like it does in the imperative programming paradigm, instead it relies on this "reduction" process I described which as you see you are free to reduce each part in whatever order you want, you'll always end up with the same thing in the end.

Re:Tour of our 250k line Clojure codebase by davidrupp on Sunday June 06, 2021 @07:17PM (#27417264)
Thanks for your willingness to engage and instruct on this. I've learned a lot. Cheers!
Re:Tour of our 250k line Clojure codebase by didibus on Sunday June 06, 2021 @07:41PM (#27417428)
It helps me to have my understandings challenged, I could have been wrong, and going through trying to explain myself helps with me better understanding things too, so cheers to you as well!
Re:Tour of our 250k line Clojure codebase by didibus on Sunday June 06, 2021 @04:14PM (#27415931)
Maybe this:

    (let [a 10
          a (+ (let [a 20] a) a)]
     a)
The thing is you can't refer to the resulting compilation, there is a correspondence between CPS and SSA, and SSA lets the JVM better optimize things.

Let me try and better explain myself. For me, the qualification here is that the form exhibit semantics that are compatible with functional programming, and can be evaluated using a lambda calculus computational model. Those semantics are in turn incompatible with the imperative ones, since it prevents you from doing some things that imperative semantics would allow, as I demonstrated with my prior example.

The let form does dictate a series of expressions to be executed sequentially from top to bottom, but the binding of their result to a name is done functionally, not imperatively.

This still models a data-flow, and the fact it lets you shadow prior names doesn't change the functional nature of it.

The form simply expresses a composition of functions and how their inputs/outputs connects.

That means it doesn't actually require any separate mutable memory location, even if the Clojure implementation for it might use some.

That's inherently the conceptual model of a functional computation, and it is not that of an imperative one.

    (let [a 1
          b 2
          a (+ a b)]
     a)
Models a data-flow that you can think of as a multitree:

      1   2
       \   \
    (+,   ,   )
     \
      3
What that means is you never need the variables, they are simply a syntactic convenience, in fact that's why they are not called variables but bindings, they are simply syntactic labels, there is no real requirement to have a seperate memory location that sequential instructions will be allowed to mutate. So how I see it, that fact makes it functional, and not imperative.

If you look at the multitree, at each level of the multitree, you are free to evaluate the nodes in any order, but if there is an input/output dependency, then you must guarantee that ordering.

At the end of the day, we could argue forever that we just have different taxonomies. I consider imperative programming the computational model where you give instructions in sequence which dictates mutations on memory locations.

I consider functional programming the computational model where you declare a composition of expressions that can be reduced using variable substitutions.

With this in mind, let qualifies as a functional construct, because it defines such a composition and can be reduced through substitution.

Now in practice, the Clojure let form reduces over this sequentially both left to right on a per-form basis, and top/down between each pair of bindings. And since Clojure allows you to mix imperative anywhere, you can have a println in it and know that it'll run after all that came before, and before all that comes after. But semantically it is still functional. And that's why you can't modify the bindings, because they do not conceptually represent memory locations which you can mutate.

Re:Tour of our 250k line Clojure codebase by davidrupp on Friday June 04, 2021 @11:16AM (#27394427)
Also, 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).
Re:Tour of our 250k line Clojure codebase by didibus on Friday June 04, 2021 @06:35PM (#27399840)
The 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.
Re:Tour of our 250k line Clojure codebase by weavejester on Friday June 04, 2021 @12:24AM (#27390530)
"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.
Re:Tour of our 250k line Clojure codebase by davidrupp on Friday June 04, 2021 @10:33AM (#27394007)
Hmm. 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.

Re:Tour of our 250k line Clojure codebase by ltultraweight on Thursday June 03, 2021 @05:09PM (#27387352)
I 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.

Re:Tour of our 250k line Clojure codebase by raspasov on Thursday June 03, 2021 @09:28PM (#27389467)
At 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".

Re:Tour of our 250k line Clojure codebase by didibus on Friday June 04, 2021 @02:07AM (#27391063)
Well 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.

Re:Tour of our 250k line Clojure codebase by AtNightWeCode on Friday June 04, 2021 @04:15AM (#27391672)
Very good explanation.