Talk

Standing on the shoulders of giants

A two-hander with a Clojure developer, working out why neither of our languages had to be written six times for six operating systems.

Event
ElixirConf Africa 2022
Date
11 to 12 August 2022
Location
Online
Length
39 minutes
  • Elixir
  • Clojure
  • BEAM
  • JVM
  • Runtimes
  • Bytecode
Sigu Magwa and Rachael Mwatela presenting at ElixirConf Africa 2022
Still from the recording of the talk, ElixirConf Africa, August 2022. Source: ElixirConfAfrica on YouTube.

Takeaways

I gave this with Rachael Mwatela, a full stack developer who came up through Clojure. The conference theme that year was Elixir as a power tool and we deliberately left the title vague so people would turn up wondering who the giant was. It is a conversation rather than a lecture. The host said so afterwards.

The giant is the runtime, which neither language built

Elixir rides the BEAM. Clojure rides the JVM. Both skipped years of work and walked into an existing ecosystem on day one. That is the whole thesis and it took us twenty minutes of groundwork to make it land.

Imagine writing Elixir once per operating system

Elixir for Windows, Elixir for Linux, Elixir for BSD, Elixir for macOS. That is the nightmare a runtime exists to prevent. Think of it as somebody else's problem that you get to not have.

You can see the delegation in the source

Open Elixir's File.open and the body calls into Erlang. Anything starting with a colon in Elixir is a call to Erlang. Elixir is not pretending the BEAM is not there.

The JVM does not exist

It is a specification. What you install is an implementation of it for your platform, which is why there is one for Linux and one for macOS, the same way there is a Chrome for each. The JRE implements the runtime; the JDK adds the tools you develop with.

Clojure's Lisp heritage is older than most countries' independence

Lisp was specified in 1958 and is still the second-oldest high-level language in use, after Fortran. Clojure arrived in 2007, compiles to classes with static methods, then to JVM bytecode.

Riding a runtime is a compounding advantage

Clojure gets Java's libraries. It also got ClojureScript by compiling to JavaScript instead and there is a Clojure on the BEAM too. Once you are hosted, retargeting is a smaller job than starting over.

Watch the talk

What a runtime is doing for you

Sigu Magwa and Rachael Mwatela on the ElixirConf Africa 2022 call, with a Podii backdrop
The two of us on the call. ElixirConf Africa ran online in 2022. Source: ElixirConfAfrica on YouTube.

We started with the nightmare version. Suppose you had to write Elixir separately for every operating system you wanted it to run on. Windows, Linux, BSD, macOS. Four languages to maintain, four sets of bugs.

A runtime is what sits on the machine where you develop and on the machine where the code eventually runs. It handles memory management, variable access and talking to the operating system. Write your Elixir on Linux and it still runs elsewhere, because someone else took on the part that differs.

Bytecode is the other half of that. Your source compresses into an intermediate form that does not care whether it is talking to your CPU, your GPU or your particular hardware. Reduce the dependency on the platform and the same artifact runs across platforms.

As a demonstration that Elixir really is delegating, we opened its source. File.open's body calls into Erlang. In Elixir anything prefixed with a colon is an Erlang call, so opening a file in Elixir is Erlang opening a file.

The JVM detour, at an Elixir conference

Rachael pointed out the obvious objection: why are we discussing Java here? Because the shape of the answer is identical and the JVM is the better-known example.

The JVM is a concept. There is no single artifact called the JVM sitting on a server. What exists is a platform-specific implementation of the specification, which is why you install one build for Linux and another for macOS, exactly like browsers. Its job is to load the code, verify it against the rules, execute it and provide the runtime environment.

Since the specification alone will not run anything, you need the implementations. The JRE is the runtime environment. The JDK is the development kit, which usually bundles the JRE and adds the compiler, the doc tool and the rest of the tooling.

Clojure, briefly and with many brackets

A slide titled Brief history of lisp and the syntax, comparing a snippet in two languages
The Lisp section, comparing the same few lines either side of the bracket question. Source: ElixirConfAfrica on YouTube.

Rachael's section. My first reaction to Clojure was the same as everyone's, which is that it is a great many brackets. It took me back to studying Scheme through Structure and Interpretation of Computer Programs.

Lisp was specified in 1958, before Kenya and most African countries were independent and remains the second-oldest high-level language still in use after Fortran. Clojure, from 2007, keeps the Lisp shape and adds dynamic typing, immutable data structures, pure functions, real concurrency support and an open development model. Rich Hickey's own framing.

The part relevant to our argument is hosting. A Clojure function compiles down to a class with static methods, then to JVM bytecode, then runs on the JVM. In exchange Clojure gets stability, performance, security and the entire existing Java library ecosystem. Calling Java from Clojure is easy.

It is used for standalone applications and for the web. The example we kept coming back to is Nubank, the Brazilian bank running on Clojure with a very large engineering team. Which has an Elixir footnote: Nubank acquired Plataformatec, the company where Elixir was born and that team went to write Clojure.

There is also ClojureScript, which is Clojure compiled to JavaScript, so the same trick applied to a second host. And, satisfyingly for the room, a Clojure that runs on the BEAM.

How Erlang and Elixir get to bytecode

The BEAM is the Bogdan/Björn Erlang Abstract Machine. If the JVM is the giant Clojure stands on, this is ours.

Erlang source goes to an abstract format, then to Core Erlang, then to bytecode. Elixir source gets its macros expanded first and then it joins Erlang's pipeline at that point. Which is the answer to why an Elixir file can call Erlang with a colon and nothing exotic happens: by then they are the same road.

A fun aside from the same slide: Joe Armstrong, at Ericsson where Erlang was born, had built a virtual machine before the JVM existed.

And the same compounding effect shows up on our side. Gleam runs on the BEAM. LFE runs on the BEAM. Erlang obviously runs on the BEAM. So does Clojure, via Clojerl, which is the point where the two halves of the talk meet.

The live Clojure section

We finished in a terminal, where I asked Rachael to prove the functional claims. It goes the way live coding goes. A stray space breaks it, the audience watches us fix it, then the mechanical keyboard gets blamed.

Threading macros

Clojure has two. They differ in whether the value is passed as the first or the second argument to the next form. Rachael made the point that for adding one to each element you do not need them at all; they earn their place once you are chaining multiply, then filter, then map.

Map

Define a list, map an anonymous function over it that adds one to each element. The bit that confused an attendee was that the resulting collection prints without commas, because Clojure treats a space as a separator. A comma also works. A semicolon does not, since that starts a comment.

Reduce, twice

First the simple form: reduce with the plus function over a list, applying it to the first two items and then carrying the result forward. Then with an explicit accumulator, starting from an empty collection, destructuring each key and value out of a map and putting the key back with its value incremented. Rachael called it destructuring; I pointed out that in Elixir we call the same thing pattern matching.

Somebody in chat added a good footnote for JavaScript developers curious about Clojure: Mori gives you Clojure's immutable data structures in JavaScript.

Rachael's closing pitch was ClojureScript, on the grounds that if you have written Ruby you will find its conventions familiar. Mine was the sentence the whole talk had been building to. Elixir is literally riding on the BEAM and that is not a weakness to apologise for. It is the reason it arrived usable.

Think your organisation has outgrown its systems?

Let's figure out what is actually broken.