![]() |
Jupyter at Bryn Mawr College |
|
|
Public notebooks: /services/public/dblank / CS245 Programming Languages / 2016-Fall / Notebooks |
If you save your notebook as a program (menu -> File -> Download as -> Python (.py)) then you can edit that file, removing everything but the final functions.
Then you can
from lab04 import *
Just a quick review of the state of Lab03:
scalc("(- 5 3)")
scalc("(/ 6 3)")
scalc("(quote 42)")
scalc("(quote (1 2 3))")
scalc("#t")
scalc("(+ #t #f)")
scalc("""(if 1 1 2)""")
scalc("""(if #f 1 2)""")
scalc("""(if #t 1 (/ 2 0))""")
sexp (structured expressions) composed of cons cells represented in Python are hard to read!
sexp([1, 2, 3])
cons(1, cons(2, cons(3, "()")))
cons(1, cons( cons(2, 4), cons(3, "()")))
One solution is to subclass list, and override the __repr__
method:
class SchemeList(list):
def __repr__(self):
retval = ""
current = self
while isinstance(current, list):
if retval:
retval += " "
retval += str(car(current))
current = cdr(current)
if current != "()": # proper list
if retval:
retval += " "
retval += ". %s" % current
return "(%s)" % retval
SchemeList(sexp([1, 2, 3]))
SchemeList([1, [SchemeList([2, 4]), [3, "()"]]])
then you only need use this method when you make a cons cell:
def cons(a, b):
return SchemeList([a, b])
And voila!
from lab05 import *
scalc("(quote (1 2 3))")
Compared to [1, [2, [3, '()']]]
the Scheme-like repr of (1 2 3)
looks quite nice! But remember that (1 2 3)
really means [1, [2, [3, '()']]]
.
Currently, we have:
def evaluator_apply(op, operands):
if op == "print":
Print(operands)
elif op == "+":
return car(operands) + cadr(operands)
else:
raise Exception("unknown apply operator: %s" % op)
What we would like to do is add all of those definitions of functions to an environment. Then a language can add to them, remove them, and change them. Then op can just be an actual Python function:
def evaluator_apply(op, operands):
return op(operands)
To do this, we need to have an "environment" where the symbols (such as "print" and "+") can be associated with their actual functions. We will define an environment (or env for short) as a list of frames where a frame is a list of symbol-function pairs, such as:
top_level_env = [
# a frame:
[
["print", Print],
["+", add_prim],
],
]
A frame is a list of bindings. We say that "+" is bound the the add_prim function. add_prim is the value bound to the symbol "+".
def lookup(symbol, env):
"""
Lookup a symbol in an environment
"""
if ...:
return ...
else:
raise Exception("no such variable: %s" % symbol)
lookup("print", top_level_env)
temp_function = lookup("print", top_level_env)
temp_function(sexp([1, 2, 3]))
def evaluator(expr, env):
if car(expr) == "lit-exp":
return cadr(expr)
elif car(expr) == "var-exp": ## NOW: look it up
return lookup(cadr(expr), env)
else:
raise Exception("invalid ast: %s" % expr)
evaluator(scalc_parse("+"), top_level_env)
scalc("(+ 5 4)")
top_level_env = [
# a frame:
[
["print", Print],
["+", add_prim],
["pi", math.pi],
],
]
def scalc(string):
return evaluator(scalc_parse(string), top_level_env)
scalc("pi")
scalc("(+ pi pi)")
A procedure (proc-exp) is:
A closure is:
scalc_parse("(lambda (i) i)")
scalc("(lambda (i) i)")
But now we have two types of functions to apply:
We must change evaluator_apply such that an op (operator) can be a closure.
When we get a closure, we must unpack the components:
Then to evaluate the body of the lambda, we extend the environment to include the symbols bound to the operands.
def evaluator_apply(op, operands, env):
if isinstance(op, list) and car(op) == "closure-exp":
symbols = cadr(op)
body = caddr(op)
closure_env = cadddr(op)
return evaluator(body,
extend_env(make_frame(symbols, operands),
closure_env))
else: # a Python function
return op(operands)
make_frame(sexp(["a", "b", "c"]), sexp([1, 2, 3]))
extend_env(make_frame(sexp(["a", "b", "c"]), sexp([1, 2, 3])), top_level_env)
scalc("((lambda (i) i) 42)")
For the next section, I had to also add eq_prim and bind that to "=" in the environment.
from lab05 import *
scalc("(= 42 43)")
SCalc doesn't support a function calling itself. Why not?
Try to write factorial:
scalc("""
((lambda (n)
(if (= n 1)
1
(* n (??? (- n 1)))))
5)
""")
What should ??? actually be? It should be the function that we are defining. But we don't have a way of referring to it.
But we could think of passing in something to act as ???:
scalc("""
((lambda (n ???)
(if (= n 1)
1
(* n (??? (- n 1) ???))))
5
???
""")
Hey, we could make a copy of the function and pass it in! We can change the ??? in the lambda to be f
and we have a complete, real function:
(lambda (n f)
(if (= n 1)
1
(* n (f (- n 1) f))))
Then we make a copy of that, and pass it in as f:
scalc("""
((lambda (n f)
(if (= n 1)
1
(* n (f (- n 1) f))))
5
(lambda (n f)
(if (= n 1)
1
(* n (f (- n 1) f))))
)
""")
Whoa. We can do recursion in our language, even though it doesn't directly support recursion. This was a trick discovered in lambda calculus. In fact, you can "factor out the recursion" in the above to discover what is called the Y-combinator:
scalc("""
(((lambda (m)
((lambda (f) (m (lambda (a) ((f f) a))))
(lambda (f) (m (lambda (a) ((f f) a))))))
(lambda (factorial)
(lambda (n)
(if (= n 1)
1
(* n (factorial (- n 1)))))))
5)
""")
Very cool!