5

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:

  1. 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?).
  2. Define a generic 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).
  3. 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:

  1. 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.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • I am unsure what you mean by "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)." can you elaborate? – juanpa.arrivillaga Mar 29 '18 at 04:45
  • @juanpa.arrivillaga Sorry, the linked blogpost is indeed a bit lengthy... I meant the `def visit(self, node): method_name = 'visit_' + type(node).__name__ [...]`-part. If I understand it correctly, it builds the `method_name` every single time the `visit`-method is invoked on the `NodeVisitor`. That in turn means that a simple evaluator of arithmetic expressions would waste an order of magnitude more time on string manipulation than on the actual computations. – Andrey Tyukin Mar 29 '18 at 11:48

1 Answers1

2

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)))))
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • Thanks for your reply! This syntax also works with `ev.register(type(Plus), lambda p: ev(p.first) + ev(p.second))`-syntax, which is reasonably concise. This is nice, but unfortunately `@singledispatch` does not work if `ev` is a method, so that I cannot hide all the `@singledispatch` and `@foobar.register` decorators in a single `Interpreter`-base class. So, maybe it would actually be easier to write a generic `Interpreter`/`Fold` using `isinstance` once, and then extend it in the concrete implementations. – Andrey Tyukin Mar 29 '18 at 15:11
  • Even though I've later found out that it does not have all the properties that I actually wanted, it was my fault, because what I actually wanted was not properly reflected in my question. Maybe I will formulate another more precise question with more requirements. This question, as it is, is answered satisfactory. Thank you again. – Andrey Tyukin Apr 02 '18 at 12:34