4
\$\begingroup\$

For learning purpose, I've written a simple lambda calculus interpreter (plus 'Add'). I would like it to be the cleanest and most idiomatic possible.

Can we make it as neat as the Haskell version?

# lambda interpreter example.
# Values: Num, Fun & Wrong.
# Terms: Cons, Var, Lam, App & Add. 
class Num:
 def __init__(self, v):
 self.v = v
 def __str__(self):
 return str(self.v)
class Fun:
 def __init__(self, f):
 self.f = f
 def __call__(self, *args, **kargs):
 return self.f(*args, **kargs)
 def __str__(self):
 return 'function'
class Wrong:
 def __str__(self):
 return 'Wrong'
def add(v1, v2):
 return Num(v1.v + v2.v)
def apply(v1, v2):
 return v1(v2)
class Cons:
 def __init__(self, v):
 self.v = int(v)
 def interp(self, env):
 return Num(self.v)
class Var:
 def __init__(self, x):
 self.x = x
 def interp(self, env):
 return env[self.x]
class Lam:
 def __init__(self, arg, body):
 self.arg = arg
 self.body = body
 def interp(self, env):
 def f(v):
 env2 = env.copy()
 env2[self.arg] = v
 return self.body.interp(env2)
 return Fun(f)
class App:
 def __init__(self, fun, param):
 self.fun = fun
 self.param = param
 def interp(self, env):
 return apply(self.fun.interp(env),
 self.param.interp(env))
class Add:
 def __init__(self, a, b):
 self.a = a
 self.b = b
 def interp(self, env):
 return add(self.a.interp(env), self.b.interp(env))
expr = App( Lam('x', Add(Var('x'), Var('x'))),
 Add(Cons(10), Cons(11)) )
print(expr.interp({}))
ferada
11.4k25 silver badges65 bronze badges
asked Sep 7, 2019 at 5:27
\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

I don't quite know what lambda calculus is (I'm assuming it's a mathematical annotation for what we might call "purely functional programming"?), but I'll give this a quick shot.

First, I'd love to have env populate itself if not provided. You really shouldn't have mutable default values for functions, though; the typical practice is to define:

interp(self, env=None):
 env = env or {}
 # ...

but that's really bloat'y in this case, so let's use a little inheritance:

class Op:
 def interp(self, env=None):
 return self._interp(env if env is not None else {})
 def _interp(self, env):
 raise NotImplementedError()
class Cons(Op):
 def __init__(self, v):
 self.v = int(v)
 def _interp(self, env): # update name to "_interp"
 return Num(self.v)
# ...
print(expr.interp()) # Yay for no boilerplate arguments!

Now we can just call interp() and the rest handles itself.

The next thing I'd do to make things a bit more concise is to leverage Python 3.7's new dataclass feature; while this doesn't seem to remove any lines of code, it's certainly more concise and descriptive, and adds some useful meta-features like allowing our AST objects to be intelligently compared and printed:

from dataclasses import dataclass
# ...
@dataclass
class App(Op):
 fun: Lam
 param: Op
 def _interp(self, env):
 return self.fun.interp(env)(self.param.interp(env))
@dataclass
class Add(Op):
 a: Op
 b: Op
 def _interp(self, env):
 return add(self.a.interp(env), self.b.interp(env))
# ...
print(expr)
# App(fun=Lam(arg='x', body=Add(a=Var(x='x'), b=Var(x='x'))), param=Add(a=Cons(v=10), b=Cons(v=11)))

Moving beyond Add, we can start using inheritance to make things clearer and more concise:

@dataclass
class BinOp(Op):
 a: Op
 b: Op
 @staticmethod
 def _func(v1, v2):
 raise NotImplementedError()
 def _interp(self, env):
 return self._func(self.a.interp(env), self.b.interp(env))
class Add(BinOp):
 @staticmethod
 def _func(v1, v2):
 return Num(v1.v + v2.v)
class Sub(BinOp):
 @staticmethod
 def _func(v1, v2):
 return Num(v1.v - v2.v)

Some minor nit-pick details to finish off:

  • 4-space indentation is more common than 2-space, which can look a bit cramped.

  • I'm not sure if it's typically allowed in lambda calculus, but I'd like to see Lam/functions that can take any number of arguments (I have a working implementation that's pretty clean that I'd be happy to share if you're interested).

answered Sep 10, 2019 at 22:29
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thanks for the tips! I'm not convinced by the env handling, tough. Too much indirections. Isn't "Explicit better than implicit"? :) Here, we really want to say the interpreter needs an environnement, and that this environnement is empty at the beginning. I'll definitively look at dataclass@. Con: even more "magic". Pro: if it becomes idiomatic, it sure can make things clearer. In lambda calculus, you handle several arguments by currying. In python: >>> m = lambda x: lambda y: x*y >>> m(6)(7) 42 \$\endgroup\$ Commented Sep 12, 2019 at 11:04
  • \$\begingroup\$ That being said, I'am interested in your multi-args implementation! \$\endgroup\$ Commented Sep 12, 2019 at 11:06

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.