Suppose that I want to write a tiny interpreter
that can evaluate expressions with binary operation
Plus
, unary operation Negate
and integer constants.
I'm currently interested only in the interpretation of the AST, so let's skip tokenization and parsing for simplicity.
In Haskell, there is a more or less canonical way to do it:
data Ast = Plus Ast Ast | Negate Ast | IntConst Int
ev :: Ast -> Int
ev (Plus a b) = (ev a) + (ev b)
ev (Negate x) = - (ev x)
ev (IntConst i) = i
main = print $ show $ ev $ (Plus (IntConst 50) (Negate $ IntConst 8))
Now, Python 3.6 does not seem to have algebraic datatypes. My problem is
that there seem to be many possible work-arounds. The most obvious one
is using isinstance
:
class Plus:
def __init__(self, first, second):
self.first = first
self.second = second
class Negate:
def __init__(self, first):
self.first = first
class IntConst:
def __init__(self, value):
self.value = value
def ev(ast):
if isinstance(ast, Plus):
return ev(ast.first) + ev(ast.second)
elif isinstance(ast, Negate):
return - ev(ast.first)
elif isinstance(ast, IntConst):
return ast.value
print(ev(Plus(IntConst(50), Negate(IntConst(8)))))
This works and prints 42
as expected, but looks somewhat noisy.
Here are a few more options, but each has some drawbacks:
- Use monkey-patching: e.g. this example
defines bunch of
???_execute
methods in the interpreter, and then attaches them to the classes that represent elements of the AST. This looks very scary to me (I don't want to know what happens if I try to execute two separate ASTs with two different interpreters in parallel: everything would break, right?). - Define a generic
NodeVisitor
that has avisit_???
-method for every type of AST-node and then does some dispatching gluing the right method name from strings and the name of the class of the instance passed to thevisit
-method. This seems somewhat more robust, but I don't like that the method names are rebuild permanently: the interpreter should concentrate on the AST, not on generating its own source code (method names). - Use some additional macro-gizmos that apparently can generate case-classes. I currently don't want to use any third-party tools, I want to have a tiny little script that is as independent from everything else as possible.
I didn't find the answer to this related question satisfactory, because the only answer just links to some external tool, which is moreover no longer maintained.
So, is there some standard way in Python 3.6.x to define interpreters for ASTs that
don't have the above mentioned drawbacks? Or should I just stick to isinstance
? Or implement good old Java-style Visitor
(not sure whether it's considered pythonic)?
EDIT
Using functools
proposed by @juanpa.arrivillaga, I came up with the following:
Use
collections.namedtuple
andfunctools.singledispatch
:from collections import namedtuple from functools import singledispatch Plus = namedtuple('Plus', ['left', 'right']) Negate = namedtuple('Negate', ['arg']) IntConst = namedtuple('IntConst', ['value']) @singledispatch def ev(x): raise NotImplementedError("not exhaustive: %r" % (type(x))) ev.register(Plus, lambda p: ev(p.left) + ev(p.right)) ev.register(Negate, lambda n: -ev(n.arg)) ev.register(IntConst, lambda c: c.value) print(ev(Plus(IntConst(50), Negate(IntConst(8)))))
However, it does not seem to work if
ev
is a method, because it cannot dispatch on theself
-argument (see this related question), so I can get only a functionev
, but no instance that represents the interperter.