This is where things start to get interesting. We will now implement support for lambda expressions, a way to build functions dynamically out of the LISP expressions we can already deal with.
A lambda expression is a list expression with a particular syntax:
(LAMBDA (arg...) expr...)
The result of evaluating a LAMBDA expression is a new
kind of object which we will call a closure. A closure can be used
in list expressions in the same way as a built-in function. In this case
the arguments will be bound to the symbols listed as arg...
in the lambda expression. The body of the function consists of the
expressions expr..., which will be evaluated in turn. The result
of evaluating the final expression is the result of applying the arguments
to the closure.
That's a pretty dense definition, so here is an example of how we would like to use lambda expressions:
(DEFINE SQUARE (LAMBDA (X) (* X X)))
SQUARE should now be a function of one argument
X, which returns the result of evaluating
(* X X). Thus evaluating (SQUARE 3)
should return 9.
We will represent the closure using a list:
(env (arg...) expr...)
env is the environment in which the closure was defined.
This is needed to allow the lambda function to use bindings without
having to pass them as arguments. For example, recall that
CAR is bound in the initial environment to our primitive
builtin_car function.
The first task is to add a new constant for the type field
of our Atom structure:
// AtomType is the enum for the type of value in a cell.
type AtomType int
const (
.
.
.
AtomType_Closure
.
.
.
)
Since the closure is just a regular list, there is no need to add anything
to value.
Like our other atom types, we will create a utility function to
initialize them. make_closure, unlike the others, performs
some validation of the arguments and so needs to return an error code.
// make_closure returns an Atom on the stack.
// a closure is a list that binds the environment and arguments.
// note that result may not be updated if there are errors.
func make_closure(env, args, body Atom, result *Atom) error {
// verify number and type of arguments
if !listp(args) || !listp(body) {
return Error_Syntax
}
// verify that all arguments are all symbols.
// if not, return an error.
for p := args; !nilp(p); p = cdr(p) {
if car(p)._type != AtomType_Symbol {
return Error_Type
}
}
// bind the environment and arguments to the closure
*result = Atom{
_type: AtomType_Closure,
value: AtomValue{
pair: &Pair{
car: env,
cdr: cons(args, body),
},
},
}
return nil
}
Next up is another special case in eval to create a
closure whenever a lambda expression is encountered.
// eval_expr evaluates an expression with a given environment and updates the result.
// note that the result may not be updated if we find errors.
func eval_expr(expr, env Atom, result *Atom) error {
.
.
.
op, args := car(expr), cdr(expr)
if op._type == AtomType_Symbol {
// evaluate special forms
if op.value.symbol.EqualString("QUOTE") {
.
.
.
} else if op.value.symbol.EqualString("DEFINE") {
.
.
.
} else if op.value.symbol.EqualString("LAMBDA") {
// verify number and type of arguments
if nilp(args) || nilp(cdr(args)) {
return Error_Args
}
return make_closure(env, car(args), cdr(args), result)
}
}
.
.
.
}
The body of our SQUARE example above is expressed in terms
of X. In order to evaluate the body, we need to create a new
environment with X bound to the value of the argument:
(closure-env (X . 3))
where the parent environment closure-env is the environment that was stored in the closure.
Finally we extend apply to create the new environment and
call eval for each expression in the body.
// apply calls a native function with a list of arguments and updates the result.
// note that the result may not be updated if we find errors.
func apply(fn, args Atom, result *Atom) error {
// handle builtins
if fn._type == AtomType_Builtin {
return fn.value.builtin.fn(args, result)
}
// handle closure
if fn._type == AtomType_Closure {
// create a new environment for the closure
env := env_create(car(fn))
// bind the arguments
for arg_names := car(cdr(fn)); !nilp(arg_names); arg_names = cdr(arg_names) {
if nilp(args) {
// not enough arguments passed in to bind against
return Error_Args
}
// put the name and value into the environment
env_set(env, car(arg_names), car(args))
// move on to the next argument
args = cdr(args)
}
if !nilp(args) {
// too many arguments to bind against
return Error_Args
}
// evaluate every expression in the body
for body := cdr(cdr(fn)); !nilp(body); body = cdr(body) {
if err := eval_expr(car(body), env, result); err != nil {
return err
}
}
return nil
}
// any other type is an error
return Error_Type
}
Let's check that our SQUARE function works as intended.
| ID | Input | Output |
|---|---|---|
| 1 | (define square (lambda (x) (* x x))) | SQUARE |
| 2 | (square 3) | 9 |
| 3 | (square 4) | 16 |
Of course, lambda expressions do not have to be bound to a symbol — we can create anonymous functions.
| ID | Input | Output |
|---|---|---|
| 4 | ((lambda (x) (- x 2)) 7) | 5 |
Fans of functional programming will be pleased to see that we can now do this kind of thing:
| ID | Input | Output |
|---|---|---|
| 5 | (define make-adder (lambda (x) (lambda (y) (+ x y)))) | MAKE-ADDER |
| 6 | (define add-two (make-adder 2)) | ADD-TWO |
| 7 | (add-two 5) | 7 |
Do you know where the value "2" is stored?
func TestChapter07(t *testing.T) {
env := env_create_default()
for _, tc := range []struct {
id int
input string
expect string
err error
}{
{id: 1, input: "(define square (lambda (x) (* x x)))", expect: "SQUARE"},
{id: 2, input: "(square 3)", expect: "9"},
{id: 3, input: "(square 4)", expect: "16"},
{id: 4, input: "((lambda (x) (- x 2)) 7)", expect: "5"},
{id: 5, input: "(define make-adder (lambda (x) (lambda (y) (+ x y))))", expect: "MAKE-ADDER"},
{id: 6, input: "(define add-two (make-adder 2))", expect: "ADD-TWO"},
{id: 7, input: "(add-two 5)", expect: "7"},
} {
var expr Atom
_, err := read_expr([]byte(tc.input), &expr)
if err != nil {
t.Errorf("%d: read error: want nil: got %v\n", tc.id, err)
continue
}
var result Atom
err = eval_expr(expr, env, &result)
if tc.err == nil && err == nil {
// yay
} else if tc.err == nil && err != nil {
t.Errorf("%d: error: want nil: got %v\n", tc.id, err)
} else if !errors.Is(err, tc.err) {
t.Errorf("%d: error: want %v: got %v\n", tc.id, tc.err, err)
}
if got := result.String(); tc.expect != got {
t.Errorf("%d: eval: want %q: got %q\n", tc.id, tc.expect, got)
}
}
}