Here's the proposed syntax, based on the same declaration in an article by Oleg:
type Exp {
| Var (Str)
| App (Exp, Exp)
| Lam (Var, Exp)
| Let (Var, Exp, Exp)
}
Semantically, the type Exp
declaration would introduce five new types into the current scope, the latter four of which are instantiable classes. (For example, Lam(Var("foo"), Var("foo"))
.) So far, nothing particularly exciting about that. As you can see, ADTs can be recursively defined without much fanfare. If they don't have a base case, you simply won't be able to instantiate one using a finite program.
The |
things stand out a little and make it clear that this is a DSL. They're also a little bit easier on the eyes than ;
at the end of the line, and visually hint that there is something very declarative going on here, rather than imperative ;
stuff.
We also introduce a case statement:
sub depth(Exp e): Int {
return case e {
| Var (_): 0
| Lam (_, e): depth(e) + 1
| App (e1, e2): max(depth(e1), depth(e2))
| Let (_, e1, e2): max(depth(e1), depth(e2))
};
}
There's both a case
statement and (as above) a case
expression. Both require an xblock. Notice how the individual cases mirror the syntax of the ADT declaration itself, although here we're binding against values instead of declaring their types. The thing after the colon can be either an expression or a block.
In the case of the App
and Let
cases above, e1
and e2
are introduced as lexical variables whose scope is only the expression or block immediately following the colon. Written as imperative code, the App
case would come out as something like this:
if type(e) == "App" {
my e1 = e.getProp(0);
my e2 = e.getProp(1);
return max(depth(e1), depth(e2));
}
The underscores simply mean "no binding". (I wanted to do asterisks, following Perl 6's lead, but it didn't look nice. Dollar signs don't make sense in 007 because we don't have sigils.) In the case of Var
we don't care about any of the properties, and we could elide the (_)
completely. But even a paren full of underscores would do shape-checking on the type, which provides a little bit of extra consistency.
I guess we could also allow multiple matchers with |
between them. Hm.
return case e {
| Var (_): 0
| Lam (_, e): depth(e) + 1
| App (e1, e2) | Let (_, e1, e2): max(depth(e1), depth(e2))
};
In this particular instance that helps us. I'm not sure it's worth the extra complexity with the matching machinery, though. We'd need to throw an error if there was a type mismatch somewhere, or if two matchers didn't introduce exactly the same variables.
If you do otherwise
as the last case, it will match when nothing else matched. Notice that you can't match on structure with this one, though; it's just a catch-all.
The compiler statically detects whether you've "covered all the cases". Given what we've said so far, that looks entirely tractable, even with things such as nested matching and multiple parameters. If you haven't covered all the cases, and don't have an otherwise
, the compiler fails and describes in poignantly descriptive prose what you missed.
I'm sorely tempted to allow individual cases to happen right inside any function or pointy block, just binding directly against its signature. Both the case
statement itself and return
could then be implicit. The depth
sub could then be written as:
sub depth(Exp _): Int {
| Var (_): 0
| App (e1, e2): max(depth(e1), depth(e2))
| Lam (_, e): depth(e) + 1
| Let (_, e1, e2): max(depth(e1), depth(e2))
}
This kind of "implicit block case matching" would go a long way to compensate for our lack of multi
things. In fact, I'd say it's a pretty competitive alternative, with a bunch of advantages of its own.
It is unclear how much we need visitors ร la #26 when we have pattern matching like this. But let's do both and see which one wins. :) Who knows, maybe they'll end up occupying different niches.