< Writings

ClojureDart: an experience report

Dynamic&
Functional&
Hosted&
Lisp

I made a mobile app with Clojure. It went great. Let’s talk about it.

(This article is based on a talk I gave at Clojure Berlin on 12 June 2024. Here are the slides. Expand for video.)

Knowuro Reader

The app is Knowuro Reader, the digital subscription version of Urology: The Last Review and now other books. The client was my friend Santiago and by extension his wife Inês. They’re both trained as doctors; he switched to tech and she is a rising star in the urology world.

The book was the result of the beautiful, precise notes she made studying for the European urology board exam. I love how carefully it is optimized for print: topics are carefully portioned onto single pages or two-page spreads, and Santiago poured a ton of design effort into getting the details of readability just right.

Nevertheless, some people and tasks are better served by an electronic version. So we decided to make a very simple reader app to complement the paper book.

The time

I started with plenty of Clojure experience, but zero knowledge of ClojureDart or Flutter beyond the fact of their existence. Working perhaps ⅔ of a standard schedule, I delivered a working prototype with all the main features in six weeks. Total calendar time to launch was eight months, which contained maybe five months of actual effort, including substantial hassle with the app stores.

Midway through those 8 months I handed the project off to Santiago so I could work on another client project. It’s a testament to both Flutter documentation and the ClojureDart project that he was shipping complex features in under a week, despite sharing my lack of specific experience.

JVM&
GraalVM&
JavaScript&
Dart

ClojureDart offers the same value proposition as any Clojure dialect: you can reach where you want with the language you want. In 2008 Rich said, “you want a lisp that clients feel comfortable deploying? Great, here’s a modernized Common Lisp for the JVM”. Reach is still the key.

(Lisp history pedantry)

For the sake of argument ignore the existence of Kawa Scheme and Armed Bear Common Lisp, and the fact that Clojure wouldn’t have been necessary were it not for Rich’s desire for immutable core data structures. Though there is a nice analogy to be made between the original Clojure’s core library innovations (e.g. abstract data structures and simplified tools for concurrency) and ClojureDart’s library of flutter macros…

That central idea of a functional, hosted, dynamic lisp was and remains persuasive enough to be extended to JavaScript, GraalVM, and now Dart.

ClojureDart’s host

To understand the target language for this new Clojure dialect, put yourself in Google’s shoes circa 2010. You’ve got a small handful of applications that tens of millions of people rely on daily, and they’re all giant piles of JavaScript. We’re talking Gmail, Ads (somewhat important!), Google Plus, Google Docs, Google Maps, that kind of thing.

Googlers were severely dissatisfied working on these huge codebases. Four major complaints rise to prominence:

Can you see how a new language would seem like a good solution? Especially when you have expert virtual machinist Lars Bak on staff. (You may know his work on the V8 engine.) A new language built on a virtual machine gives you web and server programs; mobile was originally a bonus.

DX through speed

From day one, Google prioritized developer experience in the form of near-instant hot reloading. Making dev-time performance a core value of the language was a bold move. In my opinion it paid off, and we reap the benefits of that decision in ClojureDart. (Unsurprisingly they also prioritized performance in production, both in speed and in small bundle sizes.)

Boring syntax

Another top language design priority was to be boring, boring, boring. Google is a giant organization with no time or will to teach developers new syntax. They wanted a Googler with prior experience in any ALGOL family language to be productive in Dart on day one, so conservatism in syntax won the day. Lisp it ain’t – for those of us who value language expressiveness this is disappointing. Uninnovative syntax remains an understandable choice under the circumstances.

Type system

Google also tapped Gilad Bracha to design Dart. (You may know his work on the Java language specification and the Smalltalk dialect Strongtalk.) His influence can be seen in Dart’s optional type annotations and powerful type inference. (Interestingly, the language has evolved to a stronger sound type system, which brought impressive performance gains and improved JS compilation properties.)

Erlang influence

Dart targets both the browser and a variety of multithreaded environments. So Google needed an async abstraction that would work well in both.

Inspired by Erlang's actor model, they decided on an approach based on isolates: a quarantined place where computation happens. No direct interaction is allowed; instead you communicate through message passing. Isolates are emphatically not threads, though developers familiar with threads may find the two confusingly similar.


That brings us to about 2010. In 2015 came Flutter, Google’s UI framework on top of Dart. In 2020, long-time Clojurians Christoph Grand and Baptiste Durch bet their lockdown time on exploring compiling Clojure to Dart. This led to some gnarly magic macros which provided an easy Flutter widget DSL.

Their bet worked out, and inspired others. About a year later, VC darling Roam Research wanted a mobile app. Being Clojure superfans, they bet their mobile strategy on this new dialect. Knowuro and I bet our time on ClojureDart too.

How’d it go?

The result of those bets is a lot of apps happily in production. I’m enthusiastic about ClojureDart, nevertheless it’s worth a critical review. What were some of the trade-offs?

⛔️ REPL

There’s no REPL. I’m a huge fan of the way JVM Clojure supports live programming, so this was a bummer.

Still, I admit that hot-reloading done right gives about 80% of a REPL’s benefits. (“Done right” mostly means state-preserving and fast.) It remains frustrating to so often have questions I want to ask of the running system that my app can’t answer. I understand that they’re working on REPL support and that it’s a big chunk of work.

⛔️ multimethods

There’s no support for Clojure multimethods yet. This doesn’t bother me much; I feel they’re often overused anyway.

😱 stacktraces

Presentation of errors to the developer is mixed. Error messages from ClojureDart are usually pretty good. Errors on the Dart side are pretty good too, often leveraging type inference to make helpful suggestions.

Errors originating in Flutter are nightmarish: pages upon pages of arcane framework minutiae for everything in that branch of the widget-tree. Flutter error messages themselves rarely point to the problem in a way I was able to discern. I suppose the idea is that this is mitigated by use of the widget debugger. Perhaps they’re like Java stacktraces – actually quite informative once you put a few months into understanding them.

🧐 generics & mixins

To this Clojurian, Dart’s core API and libraries seem to lean quite heavily on unnecessarily convoluted types. This was my toughest challenge in this project. I got stuck a few times trying to translate some particularly dense type signatures to ClojureDart.

Thankfully ClojureDart’s core team went above and beyond with quick and helpful responses, so I was never stuck for long. (They’re great, hire them!) These areas of confusion seemed entirely normal for my level of ignorance with those Dart features.

🙈 a little object orientation

There’s a great short paper called “Teaching Programming Languages in a Post-Linnaean Age” by Shriram Krishnamurthi. (You may know his work in the Racket community and on proglang pedagogy.) “Linnaean” means modeling a topic in a strictly categorical way, such as “Plant, Animal, or Mineral”.

A Linnean description of a programming language assigns it to a category. Krishnamurthi rejects this:

Most books rigorously adhere to the sacred division of languages into “functional”, “imperative”, “object-oriented”, and “logic” camps. I conjecture that this desire for taxonomy is an artifact of our science-envy from the early days of our discipline: a misguided attempt to follow the practice of science rather than its spirit.

We are, however, a science of the artificial. What else to make of a language like Python, Ruby, or Perl? Their designers have no patience for the niceties of these Linnaean hierarchies; they borrow features as they wish, creating melanges that utterly defy characterization.

Dart is another such melange. It aims to be object-oriented in the large, functional in the small, and scripty-feeling when the situation calls for it. Flutter embraces this with heavy object-orientation in its widget system.

This might cause a knee-jerk reaction for some Clojurians. The ClojureDart core team gives clear advice: set aside the instinct to make everything functional; embrace the Flutter way of doing things. It’s more performant and ergonomic to pass data around through the Flutter object system than to fight it and attempt to be functionally pure. I found this “when in Rome” approach to be helpful. It makes sense to follow local idiom when doing host interop, which is after all what building a Flutter app is.

✅ production-ready

I’ll hammer this point one more time because people keep asking me skeptically: yes, ClojureDart is production-ready. We and a few dozen other apps are happily in production on both iOS and Android. Come on in, the water’s fine.


Audience questions

This reminds me of early days with React Native & CLJS…

How much of a problem, or how much fun is it to use the language [with these translation challenges]?

19 times out of 20 the Flutter docs are really really good. Google did a great job here. Most Dart packages work great out of the box. On the ClojureDart side, 2/3 or more of the Flutter examples have been ported, along with other samples.

What hasn’t been ported are examples of every odd way the type system can be arranged. It took me a couple years to get comfortable porting Java type signatures to Clojure. I think the same thing is going on here. Those normal growing pains for a developer were the hardest part for me.

In general however, there are some essential trade-offs working in a hosted language. My experience was that it’s been worth it for JVM Clojure, though not in machine learning projects. ClojureScript has been more of a mixed bag. ClojureDart has been worth it too. Your mileage may vary, as they say, so you have to search in your own heart.

What does UI code look like?

Is it objects, lookup...?

Flutter widgets (objects) dominate the shape of the code, which means a lot of cljd.flutter macros, mostly f/widget. Values flow in a reactive manner from your central state atom with the :watch directive.

Interop with Flutter to look up properties of the current context is pretty easy. For example:

(f/widget
  :get {{colors .-colorScheme} m/Theme}
  …widgets…)

That binds the in-scope color scheme to colors, so you can assign, say, a Container widget a background of (.-secondary colors).

What's next for the Knowuro app?

This was an MVP we wanted to get out the door before other work came in, so there’s a lot to do. Top of the TODO list is probably a review quiz using swipe gestures.

What does the Clojure compile to?

Is it like ClojureScript where it compiles to JS, or does it produce a binary?

ClojureDart compiles (fast!) to normal Dart files in the project directory, which are watched by normal Dart tooling to provide (fast!) hot reloading. So yes, in compilation it’s more like ClojureScript than Clojure.

What's the story about integrating with Dart libraries?

And regarding complicated types, how much does that come up in day-to-day work?

Most of the time using Dart packages “just worked”. Maybe once a month it would involve some weird type thing that would stump me. Otherwise it was as easy as Java interop: add it to pubspec.yaml (the Flutter equivalent of deps.edn), restart and you’re ready to go.

One example where I was forced to do some gymnastics to satisfy the type system was Flutter’s built-in search UI, which involved reifying a SearchDelegate class. This widget is more needy than most. It required a special kind of type hint (#dart ^m/Widget) to declare a Clojure vector represented an array of widgets:

(reify :extends (m/SearchDelegate)
  (buildActions [this b-ctx]
    #dart ^m/Widget
    [(m/IconButton
      .icon (m/Icon m/Icons.clear)
      .onPressed #(.close ^m/SearchDelegate this b-ctx ""))]))

(Santiago chipped in to help answer) The Dart compiler will help tell you what kind of type hints you need when this happens.

Another example: instantiating a generic class with Object (in this case for navigation) requires some ClojureDart-specific syntax:

(#/(m/MaterialPageRoute Object)
   .builder (f/build (screen-fn)))

Would you say this made you more productive than doing it in Dart?

Do you think that's because of simplicity or easiness?

I’m reminded of a story from a friend of mine who ported a 1-liner from Ruby to Go. It ended up being 35 or so lines. That’s how I feel about ClojureDart and Dart.

What felt like the productivity boost wasn’t simplicity or ease – it was macros. Maybe call it concision, or cutting through bureaucracy? (Maybe it is simplicity after all?) Just like Clojure in 2008 with Java – say I need the colors from the Theme. Don’t make me jump through a bunch of hoops, creating and mutating variables and declaring all the exceptions this function could throw – just give me the colors from the theme!

Do you end up reading a lot of Dart?

No, not really. Once every few weeks I’d inspect the generated Dart to track down an error, but this didn’t require a lot of in-depth knowledge. I did however read quite a lot of Flutter widget documentation.

Dave Liepmann, 11 December 2024