Abandoning Async

There is an old and great schism in the OCaml community. The schism is between two concurrency libraries - Async and Lwt. As usual for these things, the two are very similar, and outsiders would wonder what the big deal is about. The fundamental problem of course is that they’re mutually incompatible. The result of this is a split OCaml world with almost no interoperability, and duplication of efforts.

The purpose of this post is not to compare the two from a technical perspective, but rather to describe my own experiences and the sentiment of the community. In the end, my conclusion is that disputes like this are rarely resolved on the basis of technical merits.

Splitting Hairs

Since I know (wish) there’s an influx of newcomers in the OCaml community. This is partly due to the great book Real World OCaml. I’ll spend a brief moment describing the fundamental differences between the 2 libraries. I assume that an informed beginner has read RWO. A basic understanding of Async and monadic concurrency will be needed to follow along.

The most noticeable between Async and Lwt is the approach towards error handling. Lwt’s analog to Async’s Deferred.t - Lwt.t, allows for an exception to be raised that would prevent an asynchronous value from being computed. Modelling this in Async we could get:

(* This is core's Result.t *)
type ('ok, 'error) result =
  | Ok of 'ok
  | Error of 'error

type Lwt.t = ('a, exn) Result.t Deferred.t

Another way of stating that is Lwt.t “inlines” the Either monad. How does Async deal with errors then? Two ways:

  • You can copy Lwt’s approach. But you must opt in to do it. See: Deferred.Or_error.t.
  • Construct monitor hierarchies that are responsible for handling exceptions raised in their context. Monitors either bubble exceptions up the monitor tree or swallow them, possibly handling them along the way.

Both of these approaches are covered in RWO.

At a first glance, Async’s approach is more elegant. When we look at the Haskell parallel universe we are reminded of the monad’s “fail” epic failure. A similar case of grafting unnecessary “features” where it does not belong in the name of error handling. Users of Lwt will quickly remind you that there are syntax extensions (both camlp4 and ppx!) to make up for this defficiency. Personally, I don’t find that satisfactory.

The second important difference between Async and Lwt that I’d like to highlight is the behavior of the most essential and primitive operation in any monadic concurrency library. The bind - >>=. async >>= f schedules the computation f using the value of async once it’s determined. However, there’s a slight twist in each library.

  • Lwt - if async is already determined when binding then f will run instantly. In effect, Lwt is always “eager” to execute as much as it can.
  • Async - attempts to help reasoning about code using the invariant “code between binds cannot be interrupted”. For example:
m >>= fun x ->
...
>>= fun y ->

You are guaranteed not to have scheduler context switch to another job in the .... The supposed benefits of this is the easier reasoning about race conditions. YMMV.

That will be it for technical details from me. There are more differences between Async and Lwt that I urge you to explore in more depth. Two great sources are:

Although I’ve tried to make the comparison above “fair and balanced”. It’s probably obvious from a purely technical perspective that I prefer Async. In fact, almost all of my concurrent programming in OCaml has been done in Async. Isn’t the decision obvious then?

A Bucket of Cold Water

First let’s quickly survey the community’s stance on Async vs. Lwt:

  • Lwt had at least 1700 downloads this month and 117 direct reverse dependencies.
  • Async had at least 400 downloads this month and 31 direct reverse dependencies. This includes Jane Street’s stuff, which is a huge chunk of it.

From the figures above and my own personal experience as a member in the community, clearly the OCaml community vastly prefers Lwt over Async. That means that if you’re an OCaml programmer that’s starting a potential project and thinking of using either concurrency library, Lwt is by far the most community friendly choice. Potential users will usually be more familiar with it and if your project is a library it will be far more interoperable. If you have any interest in being an active participant in the community then justifying using only Async will be very difficult.

What about supporting both? You have a choice between:

  • The problem space your code is trying to solve is simple enough that you can write a small core that is independent of any particular concurrency library. Daniel Bunzli’s Jsonm comes to mind.
  • You will have to functorize your library over an async monad.

As usual for these things, you cannot have your cake and eat it too.

The first option is great and is fully recommended in the odd case when it’s available. It’s certainly not free as I believe it adds complexity to the implementation. However, the cost is justified since your attempt to support both async/lwt also supports users of neither library and keeps your dependency profile low.

The second option is at first very natural. Deferred.t and Lwt.t are both monads aren’t they? Don’t we have some operations that are generic over those things? Unfortunately, this eureka moment is spoiled by a death by a thousand cuts. Be prepared to spend considerable time:

  • Learning the ins and outs of Async’s and Lwt’s idioms if you are to expose a good interface to either backend.
  • Using only the common subset of features of Async and Lwt. A painful waste of time as you end up slowly having to reimplement a concurrency library of your own as part of your software.
  • The amount of libraries available to you is much smaller. You are limited to libraries that support both Async and Lwt.
  • Tackling the considerable complexity added by supporting multiple backends in your build systems, messing around with a plethora of module signatures and sharing constraints, and worst of all - testing both backends.
  • Worst of all, consumers of your library will have to do all of this shit as well if they are to support both Async and Lwt.

All in all, it’s not same as doing 2x the work; it’s even worse! I’ve witnessed (and even participated) in seeing this churn unfold in cohttp and I can say the only reason it has been worth it is because we ended up having JS backend as well. In any case, I cannot in clear conscience recommend this path to anybody enthusiastic about using OCaml. It’s an unreasonable amount of work. The conclusion here is that it’s very unlikely that we’ll ever have an Async and Lwt compatible kumbaya in the OCaml world.

The Case for Lwt

I’ve already hinted at Lwt’s massive advantage over Async: the fact that the community has pretty much settled on using it. However, suppose that you’re some sort counter cultural hipster that writes software in a vacuum. Is there anything else Lwt offers you? Three words. Portability, portability, portability. More seriously, OCaml actually has AT LEAST 3 more platforms that Lwt supports and Async doesn’t.

  • js_of_ocaml - A very mature and downright awesome OCaml bytecode to JS compiler. While you’re at it check out the ocsigen project as well. A mature and complete (client + server) web framework for OCaml that is Lwt only.
  • mirage - Who wants to compile their server into a unikernel? I do. What’s stopping me? Async’s C dependencies. Also at this point, a considerable amount of software under mirage’s umbrella is using Lwt. Even if Async ever ends up working on mirage, it would be unwelcome as it would introduce its fragmentation there as well
  • Windows - People still use it I kid you not. In all honesty I don’t care about this one but I’m sure some of you will. Take note.
  • OCaml-Java - I’ve said at least 3 because I’m not sure if Lwt runs on this new and exciting target for OCaml. At least I’m sure of what doesn’t run on OCaml-Java. [1]

Suppose that portability is not so important. Let’s compare Async and Lwt purely as open source projects. On one hand we have a healthy active community. On the other, we have crickets. I cannot pretend to to understand why this is so, as I’ve never attempted to contribute to either project. I can tell you that only one of these projects gives me confidence that it serves the needs of its users (at least those who aren’t paid to use it).

Finally, the icing on the cake. Despite the excellent introduction to Async in RWO. Lwt remains vastly better documented than Async. Whether it’s the automatically generated reference, the numerous blog posts, or the mountains of examples everywhere. It remains that it’s much easier to get your Lwt questions answered faster.

What Now?

Naturally, I can only answer this question for myself. I will port all of my software from Async to Lwt. My minimalistic web framework Opium for one. With an eye towards running it on mirage. I will recommend everyone else who is looking to program in OCaml to use Lwt as well.

What about Core? (Or core_kernel rather)

My bearishness on Async luckily doesn’t translate to core. The core of core - core_kernel, is far more interoperable and portable. Its problems of slow compilation times and bloated executables are either solvable in the future, irrelevant, or far less severe than Async’s.

[1] I’ve been informed that Lwt doesn’t run on ocamljava

Comments

comments powered by Disqus