What Exactly is Composable?

Programmers tend to throw this word around a lot but it’s often not clear what it means. At the lowest level we have syntax, grammar, and source code. At this level almost everything is composable. Moving up a few levels we get to semantics and semantic composition and this is where things get a bit more tricky. This is the level where most of the discussions tend to happen. I’m going to use threads as an example to demonstrate the point. The code snippets will be in Ruby.

Here’s a syntactically valid piece of Ruby

Thread.new { 100.times { counter += 1 } }

You should rightfully ask a few questions now because syntactic validity is a very low bar to clear. The above piece of code isn’t actually valid. It will throw an error at runtime because we haven’t defined counter. But this code is syntatically valid so we can talk about syntactic composition. The simplest thing we can do with the above code is add more syntactically valid code to it

Thread.new { 100.times { counter += 1 } }
Thread.new { 100.times { counter += 1 } }

I duplicated the code but that’s fine because sequential composition of syntactically valid code is a perfectly fine way to “compose” code in Ruby. Let’s move up the abstraction ladder a little bit and make this code actually valid at runtime

counter = 0
Thread.new { 100.times { counter += 1 } }
Thread.new { 100.times { counter += 1 } }

The above piece of code is now valid with respect to Ruby’s runtime semantics. But there is something fishy going on here. The above code is now technically semantically valid and I can tack on as many copies of Thread.new { ... } and everything will continue to remain semantically valid with respect to Ruby’s runtime semantics.

If you’ve written code with threads then you know what the “error” is, updaing counters with += is not a valid way to compose concurrent programs because += doesn’t preserve the semantics we expect. What we expect is atomicity of updates but += is not an atomic operation.

And that brings me to my final point. When talking about composition or composability we need to define the context, the semantics, and the rules that preserve the semantics. Preemptible threads don’t respect non-atomic semantics so if we have two semantically valid programs that are correct when viewed with sequential semantics then they will not be correct when composed and viewed with concurrent/threaded semantics. This is what people mean when they say threads are not composable. They mean that sequential/non-atomic semantics are not preserved.