When I originally thought about adding pattern matching to Python, in the OCaml sense, I ended up using a decorator that more or less registed a bunch of callbacks with a dispatch table based on the types of it’s arguments.
That worked out fine, but it didn’t really have the feel of pattern matching like you get with real algebraic data types. If you recall, I was playing with the following example with the decorator approach:
type astnode = | AndNode of astnode * astnode | OrNode of astnode * astnode | NotNode of astnode | IdNode of bool let rec eval_node (n: astnode) = match n with | AndNode (l, r) -> (eval_node l) && (eval_node r) | OrNode (l, r) -> (eval_node l) || (eval_node r) | NotNode l -> not (eval_node l) | IdNode v -> v eval_node (AndNode (IdNode true, IdNode false)) (* returns false *)
The idea of that program was to create a small language to evaluate boolean expressions. In OCaml, it’s quite succinct–too succinct, in all honesty. That’s it. Of course it doesn’t include a parser, or a lexer, but that’s the crux of it.
Since that original post, I’ve posted about two other language hacks that I’ve
attempted to create–both of which use context managers and the
with-statement, worlds and dispatching urls (a la routes).
Basically, it occurred to me yesterday, that
as clause did
destructuring of tuples, in the same way that the assignment statement does.
That is to say:
a, b, c = 1, 2, 3
Will correctly assign
a = 1,
b = 2,
c = 3, in the same exact way that:
from contextlib import contextmanager @contextmanager def assign(*args): yield args with assign(1, 2, 3) as (a, b, c,): pass
a = 1,
b = 2,
c = 3.
I’ll admit, that doesn’t look very powerful by itself, but when you consider the possibilities, you might come up with something like I did:
with structural_matching((1, 2, 3)) as match: with match('list() x y z') as (x, y, z): print x, y, z with match('tuple() x _ z') as (x, z): print "tuple case" print x, z
which looks incredibly close to pattern matching in OCaml. I was super excited –but it won’t work.
match is a context manager that gets returned with the intention that
__enter__() method raises a
NoMatch exception, it skips the “body”
and goes to the next match. The problem with that thinking however is simple–
there’s no way for
__enter__ to force skipping the body due to rejected
In the example above (full source here), raising
NoMatch in the first
match block, results in control being passed back to the
__exit__() of the
outer context manager–
structural_matching. And to think, I got my hopes up!
But nevertheless, I pressed on, and hacked together, a
match, that can
destructure the following examples correctly:
with match('[1:3]', [1, 2, 3, 4]) as (a,): print a # [2, 3] with match('[1:]', "hello world") as (a,): print a # ('e', 'ello world') with match('str() x y', 'hello world') as (h, e): print 'h = ', h, ',', print 'e = ', e # h = h, e = e with match('x y z', [1, 2, 3]) as (x, y, z,): print z, y, x # 3 2 1 class obj(object): def __init__(self, x, y): self.x = x self.y = y with match('obj() .x .y', obj('x-ity', 'y-ity')) as (x, y): print 'x = ', x, ',', print 'y = ', y # x = x-ity, y = y-ity with match('x y _', [1, 2, 3]) as (x, y): print x, y # 1, 2
It’s much less useful considering you can’t put it in the
block ,like you would in a real
match statement, but it’s all we’ve got.
Back then, I concluded with This is as close to OCaml like pattern matching that we’re going to get, at least as far as I know how to get, but it’s sort of cool, and definitely a hack. Today, I’ll conclude the same way.