I’ve been busy busy busy, so I haven’t had much time to work on Effes, but I did want to share a technique I developed for mimicking algebraic sum types in Java. Those are basically what Effes calls disjunctive types: Foo | Bar
to represent an object that might be a Foo
or a Bar
.
Though they’re quite different in a lot of ways, you can think of subtypes as kinda-sorta like sum types. If you have an abstract class Animal
with exactly two subclasses, Dog
and Cat
, then an animal must either be a dog or a cat: Animal -> Dog | Cat
.
As it happens, this is how Antlr expresses alternatives within a grammar rule. For instance, my grammar includes this production:
singleType: TYPE_NAME singleTypeParameters # SingleDataType
| GENERIC_NAME # SingleGenericType
;
The classes Antlr provides include the hierarchy:
class SingleTypeContext
SingleDataTypeContext extends SingleTypeContext
SingleGenericTypeContext extends SingleTypeContext
Since the grammar is fully defined within my compiler, I know that these are the only two subclasses of SingleTypeContext; I can treat it as a closed sum type. A lot of what I do involves translating ASTs, which in turn requires looking at the specific subtype (or, in sum type terms, the type’s tag).
The approach I take to these is implemented in a Dispatcher
class, and it has three main parts:
- a mapping from subtype to a function whose input is that subtype
- a dispatcher that figures out which function to use, and invokes it
- the ability to test that all subtypes are accounted for
To take a trivial example, let’s say I were describing the AST. The first thing I’d do is to translate each node to a string, and for that I’d use the mapping functionality. I’d say that a SingleDataTypeContext maps to a function _ -> "I'm a data type!"
while SingleGenericTypeContext maps to g -> "I'm a generic named " + g.name()
. I then pass an instance typed as the superclass, SingleTypeContext, to the dispatcher. The dispatcher figures out the specific class of the instance, finds its function, downcasts it and passes it to that function.
As for the test that all subtypes are accounted for, I basically declare as an invariant in my code (which is up to me to not break) that if I want to treat a type as a sum type, all of its specific types have to be nested classes of a single enclosing class. That makes it easy to find those subtypes, which in turn makes it easy to verify that they’re all in the mapping.
My code has a few places where I use the Dispatcher, but one example (with shorthand Java, to fit in a blog format) is in ExpressionCompiler
, where I parse an “expression line” (basically a one-line expression or the first line of a case
statement):
private static final Dispatcher<~> exprLineDispatcher
= Dispatcher.builder(/*...*/)
.put(SingleLineExpressionContext.class, ExpressionCompiler::singleLine)
.put(CaseExpressionContext.class, ExpressionCompiler::caseExpression)
.build(ExpressionCompiler::exprLineErr); // default, for err handling
Needless to say, this is not the most efficient approach; it’s basically a visitor-style dispatch, but backed by a HashMap instead of Java’s built-in polymorphism. For my (current) needs, it’s good enough. It’s also quite convenient for when I add a new alternative to a production in the grammar, since a quick run of the find-all-subtypes test tells me exactly where I need to add new code to handle that alternative.