Solving the Expression Problem
↩ ↪October 01, 2010
I started working on Magpie out of frustration with a lot of the languages I used. One of the key itches I wanted to scratch is something called the expression problem. The original formulation of it isn’t very helpful to someone not writing a compiler, so I’ll recast it to something that’s a little more tangible and relevant to the kind of code you find yourself writing.
The core problem is one of extension: How do you make it easy to add both new datatypes and new behaviors to an existing system?
Let’s say we’re writing a document editor. We’ve got a few kinds of documents that it can work with: Text, Drawings, and Spreadsheets. And we’ve got a few operations we need to be able to do with a document: draw it to the screen, load it, and save it to disc. They form a grid, like so:
Text Drawing Spreadsheet
┌───────────┬───────────┬───────────┐
draw() │ │ │ │
├───────────┼───────────┼───────────┤
load() │ │ │ │
├───────────┼───────────┼───────────┤
save() │ │ │ │
└───────────┴───────────┴───────────┘
Each cell in that grid is a chunk of code we’ve got to write. We need to draw text, load a drawing, save a spreadsheet, etc. All nine combinations are functions that need to be implemented or we’ll have problems if we’re trying to deal with documents generically.
There are a couple of questions to answer:
How do we organize the code for this?
How do we add new columns—new types of documents?
How do we add new rows—new operations you can perform on any document?
How do we ensure all of the cells are implemented?
The way you’ll answer those is strongly influenced by your choice of language. In many ways language paradigms differ exactly in how they answer just those questions. For our purposes, we’ll only care about three flavors:
Statically typed object-oriented languages
These are the most popular languages on the block today, and include C++, Java, and C#. They organize code into classes, and define operations as methods on those classes. A Java implementation of the above table looks something like:
public interface Document {
void draw();
void load();
void save();
}
public class TextDocument implements Document {
public void draw() { /* Draw text doc... */ }
public void load() { /* Load text doc... */ }
public void save() { /* Save text doc... */ }
}
public class DrawingDocument implements Document {
public void draw() { /* Draw drawing... */ }
public void load() { /* Load drawing... */ }
public void save() { /* Save drawing... */ }
}
public class SpreadsheetDocument implements Document {
public void draw() { /* Draw spreadsheet... */ }
public void load() { /* Load spreadsheet... */ }
public void save() { /* Save spreadsheet... */ }
}
An object-oriented language answers question 1 by saying that all operations for a single type should be lumped together. Everything you can do with a spreadsheet— drawing, loading, and saving— goes together in the same class and typically the same file. The downside is that the operations are smeared across the codebase. If you want to see how all file saving is handled, you need to look at three files.
Question 2 is easy: you define a new class that implements the interface (or inherits from a base class). Object-oriented languages are good at this. You can do this even if the base class or interface is in some other library.
Question 3 is a bit tougher. Let’s say we decide we want to add support for
printing. We’ll have to add a print()
method to our base Document
interface
and then touch every file that implements it. Gross. If Document
happens to be
defined in code we don’t control, we’re out of luck.
Even worse, it means we tend to put things in classes that don’t really belong together. Do we really want to mix the logic for rendering and interacting with the file system in the same class? There are solutions and patterns to mitigate this, but they’re complex and awkward (I’m looking at you, Visitor Pattern).
But at least question 4 is easy. The compiler tells us if we don’t fully
implement an interface, so if we declare that a class implements Document
then
we can be sure that all of the cells in the grid are covered.
Statically typed functional languages
Let’s see how the other half lives. Languages in the ML family like Haskell and F# tend to divide things up differently. Where an object-oriented language breaks the grid along column boundaries, a functional language breaks it into rows.
This even explains the names of the paradigms: Object-oriented languages place emphasis on objects (the columns). Functional languages place emphasis on the functions (the rows).
A Caml implementation of our example looks like:
type document
= Text
| Drawing
| Spreadsheet
fun draw (Text) = (* Draw text doc... *)
| draw (Drawing) = (* Draw drawing doc... *)
| draw (Spreadsheet) = (* Draw spreadsheet... *)
fun load (Text) = (* Load text doc... *)
| load (Drawing) = (* Load drawing doc... *)
| load (Spreadsheet) = (* Load spreadsheet... *)
fun save (Text) = (* Save text doc... *)
| save (Drawing) = (* Save drawing doc... *)
| save (Spreadsheet) = (* Save spreadsheet... *)
(At least, I hope that’s right. Please let me know what I get wrong.)
The document
interface has become an algebraic datatype with cases for
the different concrete document types. Each operation is a single function
that uses pattern matching to select behavior appropriate for that type.
In other words, we switch up its answer to the first question. Functions are
lumped together, with a single draw
function having the logic to draw all
different types of documents together. This keeps different kinds of behavior
nicely isolated from each other—these functions could be put into different
files without any problem.
Question 3 is answered easily: just define a new function somewhere that handles
all of the different document types. Question 2 is where the pain is. If we add
a new document type, we have to add a new case to the datatype and touch every
function in the codebase to handle that case. If the core document
datatype is
defined in code we don’t control, we’re hosed again.
Again, though, static typing helps us with question 4: the compiler will tell us if one of these functions doesn’t match a document type. So there’s no net win between the two, we’ve just changed how we slice the same cake. Let’s look at a third option:
Dynamically typed languages
Way on the other side of town are dynamic languages like Python, Ruby and Javascript (and their non-OOP progenitors like Scheme, but I’ll focus on OOP ones here because that’s what I’m most familiar with). They’re super flexible and tons of fun to code in. How do they stack up?
The big win is that you can generally organize your code how you want and add both new operations and new types with impunity. The normal case is to organize things like a static OOP language where all of the operations for a type are lumped together into one file.
However, the dynamism gives you more flexibility. If you want to add a new operation to existing types, you have the freedom to do so outside of the file where that type is defined. You can add new methods into existing classes. This lets you, for example, pull the save/load logic from our document classes out into separate files but then mix that back into the original classes so they’re still as easy to use. No visitor pattern in sight.
So that leaves one last question: How do we ensure all of the squares are covered? And that’s when the sad trombone comes in. There is no compile-time checking for this. The best you can do is write lots of tests and hope you’re covered.
Call me crazy, but I’m not happy with any of these solutions. I want the simplicity of defining a class and putting its core operations all in one place. At the same time, I want to be able to mix in new methods into classes outside of the file where its defined. I want to group some code by row (operation) and other code by column (type), each where it makes the most sense.
And once all that’s done, I want the language to be smart enough to tell if I forgot something or messed something up.
Magpie = open classes + static checking
Here’s how you accomplish this in Magpie. First, we define an interface that all documents will implement:
interface Document
draw()
load()
save()
end
Then we create some classes that implement them:
class TextDocument
draw() // Draw text doc...
load() // Load text doc...
save() // Save text doc...
end
class DrawingDocument
draw() // Draw drawing...
load() // Load drawing...
save() // Save drawing...
end
class SpreadsheetDocument
draw() // Draw spreadsheet...
load() // Load spreadsheet...
save() // Save spreadsheet...
end
So far, this looks pretty much like the static OOP solution with a bit less
boilerplate. The biggest difference is that there’s no explicit implements Document
on the classes. In Magpie, if a class has all of the methods that an
interface requires, then it is automatically considered to implement the
interface.
When you try to use the concrete class in a place where the interface is
expected, the interpreter checks to make sure that the class implements it. Note
that it does this statically, before main()
has ever been called, like a
typical static language.
Extending a class
Here is where it gets interesting. Now we decide we want to add printing support. In Magpie, classes and interfaces are open for extension. So we can just do:
extend interface Document
print()
end
If we try to run the program now, we get type check errors every place we pass a
concrete document class to something that expects the interface: the classes no
longer implement Document
since they lack the required print()
method. To
patch that up, we implement those:
def TextDocument print() // Print text doc...
def DrawingDocument print() // Print drawing...
def SpreadsheetDocument print() // Print spreadsheet...
(def
is one of two syntaxes for adding members to a class. It’s nice for
adding a single member to a class. If you’re adding a bunch of members to one
class, you can also do extend class
which works like a regular class
definition but adds to an existing class.)
We can do this wherever we like, in any file. This lets us keep all of the code for printing lumped together and isolated from the rest of the code just like a dynamic language.
The magical part is that this is statically type checked too. The program won’t run until we’ve made sure that every document type now has all four methods.
Magpie’s answers for the original four questions are:
How do we organize code? However you like. Put stuff together where it makes sense.
How do we add new columns—new types of documents? Like a typical OOP language: define a new class. If it has the necessary methods, it’s a
Document
.How do we add new rows—new operations you can perform on any document? Add new methods to the classes that need them. This can be done outside of the file where the class is defined.
How do we ensure all of the cells are covered? Add the new operation to the interface too. The static checker will then make sure only classes that have the operation are used in places that expect a
Document
.
When you’re defining things, you get the flexibility of a dynamic language. Before it runs, though, you get the safety of a static language.