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:
???_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?).NodeVisitor
that has a visit_???
-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 the visit
-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).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
and functools.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 the self
-argument (see this related question), so I can get only a function ev
, but no instance that represents the interperter.
If cleaner code is what you are looking for, I think the functools.singledispatch
decorator would work in this case:
import functools
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
@functools.singledispatch
def ev(ast):
raise NotImplementedError('Unsupported type')
@ev.register(Plus)
def _(ast):
return ev(ast.first) + ev(ast.second)
@ev.register(Negate)
def _(ast):
return -ev(ast.first)
@ev.register(IntConst)
def _(ast):
return ast.value
print(ev(Plus(IntConst(50), Negate(IntConst(8)))))