Tuesday, November 24, 2020

clojure.core.async: An unfulfilled promise

Disclaimers:

  • Its my first (ever) technical blog post, first every in English
  • I am no natural English speaker, so read this with a Hebrew accent in your mind, and do - excuse my blunt style.
  • The subject is extremely outdated, but has been sitting in my head for a (very) long time and only now as I start writing, I'm spitting it out.

I'll start by stating the obvious:

Rich Hickey is one of the most influential software development philosopher of our time

If not on the world - with no doubt on me, just ask anyone I worked with or talked tech to since around 2009 how much I've been babbling about it. I'm a true follower.

Nothing has affected the way I write code more than the Clojure language. Each and every talk of Rich Hickey (and I try to see all of them, numerous times, advise you to do so as well) has changed the way I see the topic at hand and software as a whole.

Why I chose the title Unfulfilled promise? First of all: the obvious pun. But mainly,  the core.async library (and Clojure in the whole) is an amazing idea that remains, in my world, unfulfilled: I only use it for toying around. Continue reading to understand (some) of why.

Ever Since I first met Clojure (probably circa 2009), I toy around with Clojure often (I even wrote part of my Geometry thesis in Clojure, but more about that in a future post) and yet:

   I never choose to use Clojure, not even in my toy projects.

This basically makes me a professional software engineer - Clojure hobbyist. The only way to read this post is to understand I am truly not a professional Clojure developer. In some manner this post may shed some light on why some hobbyists do no turn professionals in the Clojure stack. I do not think it is important for a language to be considered industry leading and it is great for a developer to have educational stacks like the Clojure stack is for me. I know that it is very common in the industry to use Clojure, but for me it remains educational only. This post details one aspect of why that remains such.

That aspect is Ease of debugging in the common machines it targets (I'll focus the browser here)

I know for a fact that Clojure is very common in the industry. This Blog post is not regarding whether Clojure is a good fit for others - I am confident it is. It is regarding a very specific aspect I find missing in the stack in order for me to choose it as something I would use if I had to choose: the ease of debugging when you encounter the bad path of things.

This blog post will try and explore the above via a specific talk Hickey gave a few years ago on a specific, ingenious library/framework, namely core.async.

I strongly suggest not to continue reading beyond this point without seeing the talk as specific references will start now.

Is core.async instead of, say, Observables, in fact easier in the bad path when a problem needs to be debugged, given the layers of transpiling and macro expansion one needs to dig through?

I'll start by stating a few amazing properties of core.async:

  • It allows state machines to be written in a very natural way even when they stumble upon the need to blockingly wait for other state machines which is infinitely amazing (see the talk, please do)
  • It allows apparently blocking code to be written in languages that do not allow threading such as javascript
  • CSP is amazing, and core.async offers CSP in the browser and CSP without tons of threads in the JVM.

The flipside is in the ingenious implementation. The exact property that makes the above possible is exactly what makes it so hard to comprehend, its magic.

All of the above is achieved with some LISPy magic called hygenic macros. Macros are basically tools that rewrite your code in the pre-processing phase, just before it is compiled using transformations that you (or the framework developers) wrote for you. In other words, the Macro mechanism is "pluggable" and not baked into the language. Macros allow a language to be extensible, which is exactly what Clojure is. However, there is a flip side which mainly has to do with creating a larger difference between what is run on the machine (JVM / javascript / CLI / Python) to what you originally wrote. Therefore, all the core.async "magic", that is backed by transformations you prefer not being aware of is exactly what stands between your original code and you are now debugging. This added complexity, in my view is simply moving the complexity Rich is speaking about in the talk to opaque layers which you now have to understand in order to debug already very complex code. I'll re-iterate it in two bullets:

  • Rich offers a more natural way of writing concurrency. Well: there is nothing natural about running LISP on procedural machines. This comes from someone who loves LISP dearly, had it been run on LISP-aware machines with proper debug and trace tools, etc. (Racket for example). 
  • In order to achieve the above magic, there are many "layers" or code rewriting (complex macro expansion) + transpiling in the js env /compiling in the JVM so basically what you get is amazing but very hard to debug since code since it is extremely far from what is eventually run. In my humble experience source maps do not really solve it in the browser environment. To be honest I never really tried it in the JVM but I gather its more complex as it compiles to byte code. I cannot emphasize how important for me is the ease of debugging. Moreover, what Rich offers in the talk is exactly that - a library easier to debug than "callback hell" and "place oriented programming".

One might argue: "Hey, any compiling does exactly the above. Moreover, any use of framework". For that I answer: 

Compilers: It is obvious, for me at least, that interpreted languages are easier to debug. Complex macros create a very hard challange for debugging.

As for frameworks: generally the same answer. Since using a framework is an obvious must, I argue that non-macro based frameworks are much much easier to comprehend and debug as they do not change the code under your feet and sending traces (logs) is generally much more naturally achieved.

 So where do we go from here if we do want apprent blocking code in the browser?

I'll use a famous idiom Rich hickey also uses often:

There is no silver bullet

As I write this, I'm trying to bake (or Google) a way of writing seemingly blocking js code that involves less transpiling and preprocessing,

It cannot, naturally,  promise the ease of use of core.async promises, but is easier to debug with the browser debugger. I will fully bake the idea and write a blog post shortly.

With a little abuse of notation, the search can loosely be said to be inspired by Hickey's monumental talk Simple made Easy.

No comments:

Post a Comment

clojure.core.async: An unfulfilled promise

Disclaimers: Its my first (ever) technical blog post, first every in English I am no natural English speaker, so read this with a Hebrew acc...