I decided to learn Clojure after having learnt Objective-C (I wanted the C preprocessor to be able to handle C itself as a macro language) and was already into quite advanced programming stuff (last thing I did in Objective-C was a library to code in an aspect oriented fashion).
I started learning Clojure straight with 4Clojure challenges, no book, just a cheatsheet of common Clojure functions and apart from having to learn how to use basic items like let
from the problems’ solutions which was a little disorientating at first, the biggest adaptation I had to do was to think functionally, i.e. with code flow explicitly in the form of a directed acyclic graph.
At first, I literally had to draw the graph prior to writing code, then I quickly got used to thinking in these terms. Eventually I learnt about libraries like Prismatic’s Graph that explicitly treat and present code as a graph.
This is what I love with Clojure (and what somehow prevents me from using other lisps): the flow of syntax matches the flow of data most of the time. To put it in other way you have several “domains”, that of unexpanded code, that of code, and that of functions, with steps that functionally link one domain to the next one:
unexp. code -- [reader-macros + macro-expansion] --> code -- [compilation] --> functions
The items in these domains are linked through a directed acyclic graph (respective to each domain). Code, both expanded and unexpanded, is tree-ish. Ditto for functions. You have a many-to-one, branches-to-root, inputs-to-output underlying structure to both.
Now what’s interesting is that transformation between these domains preserves order relationships between elements from the source domain to their counterpart in the target domain so that you can trivially reason on order relationships between items from distinct domains.
- A macro can only modify code that is <= to the processed form (
&form
): it cannot transform outer code. This is due in part to the fact macros are functions under the hood.
- Side-effects excluded, data transformations only happen downward in functions too: a function cannot change a variable outside of its scope, and by scope I mean lexical, i.e. syntactically ordered, scope.
What does this lead to ?
To put it in other words, each step in transforming Clojure code and data, from reading it to compiling it, as well as “mutating” it (i.e. creating a new version of it), can be seen as graph transformations on “structured” graphs (i.e. DAGs), specified as DAGs themselves.
And this is really interesting because this is the shape of causality. I often hear that no matter what, FP is implemented on imperative bases. However true this may be, an imperative program where cause and effects are fully tracked and analyzed will take the shaped of a DAG.