这是我们一直在努力的方向。 这是第三幕。 我们将向解释器添加对函数和函数调用的支持。 完成本节后,我们将能够在 REPL 中执行此操作:
>> let add = fn(a, b, c, d) { return a + b + c + d };
>> add(1, 2, 3, 4);
10
>> let addThree = fn(x) { return x + 3 };
>> addThree(3);
6
>> let max = fn(x, y) { if (x > y) { x } else { y } };
>> max(5, 10)
10
>> let factorial = fn(n) { if (n == 0) { 1 } else { n * factorial(n - 1) } };
>> factorial(5)
120
如果这没有给你留下深刻印象,那么看看这个。 传递函数、高阶函数和闭包也可以:
>> let callTwoTimes = fn(x, func) { func(func(x)) };
>> callTwoTimes(3, addThree);
9
>> callTwoTimes(3, fn(x) { x + 1 });
5
>> let newAdder = fn(x) { fn(n) { x + n } };
>> let addTwo = newAdder(2);
>> addTwo(2);
4
是的,没错,我们将能够做到所有这些。
为了从我们目前所处的位置到达那里,我们需要做两件事:在我们的对象系统中定义函数的内部表示,并添加对 Eval 函数调用的支持。
不过别担心。 这很简单。 我们在上一节中所做的工作现在得到了回报。 我们可以重用和扩展我们已经构建的很多东西。 在本节中的某个时刻,您会看到很多东西刚刚开始融合在一起。
既然“一次一步”把我们带到了这里,现在没有理由放弃这个策略。 第一步是处理函数的内部表示。
在内部表示函数的需要来自这样一个事实,即 Monkey 中的函数被视为任何其他值:我们可以将它们绑定到名称,在表达式中使用它们,将它们传递给其他函数,从函数中返回它们等等。 和其他值一样,函数在我们的对象系统中需要一个表示,所以我们可以传递、分配和返回它们。
但是我们如何在内部将一个函数表示为一个对象呢? 我们对 ast.FunctionLiteral 的定义给了我们一个起点:
// ast/ast.go
type FunctionLiteral struct {
Token token.Token // The 'fn' token
Parameters []*Identifier
Body *BlockStatement
}
我们不需要函数对象中的 Token 字段,但 Parameters 和 Body 是有意义的。 如果没有函数体,我们无法评估函数,如果我们不知道函数具有哪些参数,我们也无法评估函数体。 除了参数和正文之外,我们还需要在新的函数对象中添加第三个字段:
// object/object.go
const (
// [...]
FUNCTION_OBJ = "FUNCTION"
)
type Function struct {
Parameters []*ast.Identifier
Body *ast.BlockStatement
Env *Environment
}
func (f *Function) Type() ObjectType { return FUNCTION_OBJ }
func (f *Function) Inspect() string {
var out bytes.Buffer
params := []string{}
for _, p := range f.Parameters {
params = append(params, p.String())
}
out.WriteString("fn")
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(") {\n")
out.WriteString(f.Body.String())
out.WriteString("\n}")
return out.String()
}
object.Function 的此定义具有 Parameters 和 Body 字段。 但它也有 Env,一个包含指向对象的指针的字段.Environment,因为 Monkey 中的函数带有它们自己的环境。 这允许闭包,它“关闭”它们定义的环境,以后可以访问它。 当我们开始使用 Env 字段时,这会更有意义。 你会看到的。
定义完成后,我们现在可以编写一个测试来断言我们的解释器知道如何构建函数:
// evaluator/evaluator_test.go
func TestFunctionObject(t *testing.T) {
input := "fn(x) { x + 2; };"
evaluated := testEval(input)
fn, ok := evaluated.(*object.Function)
if !ok {
t.Fatalf("object is not Function. got=%T (%+v)", evaluated, evaluated)
}
if len(fn.Parameters) != 1 {
t.Fatalf("function has wrong parameters. Parameters=%+v",
fn.Parameters)
}
if fn.Parameters[0].String() != "x" {
t.Fatalf("parameter is not 'x'. got=%q", fn.Parameters[0])
}
expectedBody := "(x + 2)"
if fn.Body.String() != expectedBody {
t.Fatalf("body is not %q. got=%q", expectedBody, fn.Body.String())
}
}
此测试函数断言,对函数字面量求值会返回正确的 *object.Function,并具有正确的参数和正确的主体。 该函数的环境稍后将在其他测试中隐式地进行测试。 只需将几行代码以新的 case 分支的形式添加到 Eval 即可使此测试通过:
// evaluator/evaluator.go
func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
case *ast.FunctionLiteral:
params := node.Parameters
body := node.Body
return &object.Function{Parameters: params, Env: env, Body: body}
// [...]
}
很简单,对吧? 测试通过。 我们只是重用了 AST 节点的 Parameters 和 Body 字段。请注意我们在构建函数对象时如何使用当前环境。
通过相对较低级别的测试,从而确保我们正确构建了函数的内部表示,我们可以转向函数应用的主题。 这意味着,扩展我们的解释器以便我们可以调用函数。 对此的测试更具可读性和更容易编写:
// evaluator/evaluator_test.go
func TestFunctionApplication(t *testing.T) {
tests := []struct {
input string
expected int64
}{
{"let identity = fn(x) { x; }; identity(5);", 5},
{"let identity = fn(x) { return x; }; identity(5);", 5},
{"let double = fn(x) { x * 2; }; double(5);", 10},
{"let add = fn(x, y) { x + y; }; add(5, 5);", 10},
{"let add = fn(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20},
{"fn(x) { x; }(5)", 5},
}
for _, tt := range tests {
testIntegerObject(t, testEval(tt.input), tt.expected)
}
}
这里的每个测试用例都做同样的事情:定义一个函数,将其应用于参数,然后对产生的值进行断言。 但是由于它们的细微差别,它们测试了多个重要的事情:隐式返回值、使用 return 语句返回值、在表达式中使用参数、多个参数以及在将参数传递给函数之前评估参数。 我们还在此处测试了 *ast.CallExpression 的两种可能形式。 一个函数是一个标识符,它计算为一个函数对象,第二个函数是一个函数文字。 整洁的事情是它并不重要。 我们已经知道如何评估标识符和函数文字:
// evaluator/evaluator.go
func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
case *ast.CallExpression:
function := Eval(node.Function, env)
if isError(function) {
return function
}
// [...]
}
是的,我们只是使用 Eval 来获取我们想要调用的函数。 无论是 *ast.Identifier 还是 *ast.FunctionLiteral:Eval 都会返回一个 *object.Function(当然,如果没有错误)。
但是我们如何调用这个 *object.Function 呢? 第一步是评估调用表达式的参数。 原因很简单:
let add = fn(x, y) { x + y };
add(2 + 2, 5 + 5);
这里我们希望将 4 和 10 作为参数传递给 add 函数,而不是表达式 2 + 2 和 5 + 5。评估参数只不过是评估表达式列表并跟踪生成的值。 但是我们也必须在遇到错误时立即停止评估过程。 这将我们引向以下代码:
// evaluator/evaluator.go
func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
case *ast.CallExpression:
function := Eval(node.Function, env)
if isError(function) {
return function
}
args := evalExpressions(node.Arguments, env)
if len(args) == 1 && isError(args[0]) {
return args[0]
}
// [...]
}
func evalExpressions(
exps []ast.Expression,
env *object.Environment,
) []object.Object {
var result []object.Object
for _, e := range exps {
evaluated := Eval(e, env)
if isError(evaluated) {
return []object.Object{evaluated}
}
result = append(result, evaluated)
}
return result
}
这里没什么好看的。 我们只是迭代一个 ast.Expressions 列表,并在当前环境的上下文中评估它们。 如果遇到错误,我们将停止评估并返回错误。 这也是我们决定从左到右评估参数的部分。 希望我们不会在 Monkey 中编写代码来断言参数评估的顺序,但如果我们这样做,我们就处于编程语言设计的保守和安全方面。
所以! 现在我们有了函数和求值参数列表,我们如何“调用函数”? 我们如何将函数应用于参数?
显而易见的答案是我们必须评估函数体,它只是一个块语句。 我们已经知道如何评估这些,那么为什么不直接调用 Eval 并将其传递给函数体呢? 一个字:争论。 函数体可以包含对函数参数的引用,仅在当前环境中评估函数体会导致对未知名称的引用,这会导致错误,这不是我们想要的。 在当前环境下,按原样评估身体是行不通的。
我们需要做的是改变评估函数的环境,所以函数体中对参数的引用解析为正确的参数。 但是我们不能只是将这些参数添加到当前环境中。 这可能会导致之前的绑定被覆盖,这不是我们想要的。 我们希望这样做:
let i = 5;
let printNum = fn(i) {
puts(i);
};
printNum(10);
puts(i);
使用打印行的 puts 函数,这应该打印两行,分别包含 10 和 5。 如果我们在评估 printNum 的主体之前覆盖当前环境,最后一行也会导致打印 10。
因此,将函数调用的参数添加到当前环境以使其在函数体中可访问是行不通的。 相反,我们需要做的是保留以前的绑定,同时提供新的绑定——我们称之为“扩展环境”。
扩展环境意味着我们创建一个 object.Environment 的新实例,并带有指向它应该扩展的环境的指针。 通过这样做,我们用现有的环境包围了一个新鲜空旷的环境。
当调用新环境的 Get 方法并且它本身没有与给定名称关联的值时,它会调用封闭环境的 Get。 这就是它正在扩展的环境。 如果封闭环境找不到值,它会调用自己的封闭环境,依此类推,直到不再有封闭环境,我们可以安全地说我们有一个“错误:未知标识符:foobar”。
// object/environment.go
package object
func NewEnclosedEnvironment(outer *Environment) *Environment {
env := NewEnvironment()
env.outer = outer
return env
}
func NewEnvironment() *Environment {
s := make(map[string]Object)
return &Environment{store: s, outer: nil}
}
type Environment struct {
store map[string]Object
outer *Environment
}
func (e *Environment) Get(name string) (Object, bool) {
obj, ok := e.store[name]
if !ok && e.outer != nil {
obj, ok = e.outer.Get(name)
}
return obj, ok
}
func (e *Environment) Set(name string, val Object) Object {
e.store[name] = val
return val
}
object.Environment 现在有一个名为 outer 的新字段,它可以包含对另一个 object.Environment 的引用,它是封闭环境,它正在扩展。 NewEnclosedEnvironment 函数使创建这样一个封闭环境变得容易。 获取方法也被改变了。 它现在也检查给定名称的封闭环境。
这种新行为反映了我们对变量作用域的看法。 有内部作用域和外部作用域。 如果在内部作用域中找不到某些内容,则会在外部作用域中查找。 外部作用域包含内部作用域。 并且内部范围扩展了外部范围。
使用我们更新的 object.Environment 功能,我们可以正确评估函数体。 请记住,问题在于:将函数调用的参数绑定到函数的参数名称时,可能会覆盖环境中现有的绑定。 现在,我们不是覆盖绑定,而是创建一个由当前环境包围的新环境,并将我们的绑定添加到这个新的空环境中。
但是我们不会使用当前环境作为封闭环境,不。 相反,我们将使用 *object.Function 携带的环境。 还记得那个吗? 这就是我们定义函数的环境。
这是 Eval 的更新版本,它可以完全正确地处理函数调用:
// evaluator/evaluator.go
func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
case *ast.CallExpression:
function := Eval(node.Function, env)
if isError(function) {
return function
}
args := evalExpressions(node.Arguments, env)
if len(args) == 1 && isError(args[0]) {
return args[0]
}
return applyFunction(function, args)
// [...]
}
func applyFunction(fn object.Object, args []object.Object) object.Object {
function, ok := fn.(*object.Function)
if !ok {
return newError("not a function: %s", fn.Type())
}
extendedEnv := extendFunctionEnv(function, args)
evaluated := Eval(function.Body, extendedEnv)
return unwrapReturnValue(evaluated)
}
func extendFunctionEnv(
fn *object.Function,
args []object.Object,
) *object.Environment {
env := object.NewEnclosedEnvironment(fn.Env)
for paramIdx, param := range fn.Parameters {
env.Set(param.Value, args[paramIdx])
}
return env
}
func unwrapReturnValue(obj object.Object) object.Object {
if returnValue, ok := obj.(*object.ReturnValue); ok {
return returnValue.Value
}
return obj
}
在新的 applyFunction 函数中,我们不仅检查我们是否真的有一个 *object.Function ,而且还将 fn 参数转换为 *object.Function 引用,以便访问函数的 .Env 和 .Body 字段(哪个对象 .Object 没有定义)。
extendFunctionEnv 函数创建一个新的 *object.Environment,它被函数的环境包围。 在这个新的封闭环境中,它将函数调用的参数绑定到函数的参数名称。
这个新封闭和更新的环境就是评估函数体的环境。 如果此评估的结果是 *object.ReturnValue,则解包。 这是必要的,否则返回语句会冒泡通过几个函数并停止所有函数的评估。 但我们只想停止对最后一个被调用函数体的评估。 这就是为什么我们需要解开它,这样 evalBlockStatement 就不会停止评估“外部”函数中的语句。 我还在之前的 TestReturnStatements 函数中添加了一些测试用例,以确保它有效。
那些是最后丢失的部分。 什么? 真的吗? 是的! 看看这个:
$ go test ./evaluator
ok monkey/evaluator 0.007s
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> let addTwo = fn(x) { x + 2; };
>> addTwo(2)
4
>> let multiply = fn(x, y) { x * y };
>> multiply(50 / 2, 1 * 2)
50
>> fn(x) { x == 10 }(5)
false
>> fn(x) { x == 10 }(10)
true
什么? 是的! 有用! 我们现在终于可以定义和调用函数了! 有句话叫“这没什么好写的”。 嗯,这是! 但在我们戴上派对帽子之前,值得仔细研究一下函数与其环境之间的交互以及它对函数应用的意义。 因为我们所看到的并不是我们所能做的,还有更多。
所以,我敢打赌,有一个问题仍然困扰着您:“为什么要扩展函数的环境而不是当前环境?” 简短的回答是这样的:
// evaluator/evaluator_test.go
func TestClosures(t *testing.T) {
input := `
let newAdder = fn(x) {
fn(y) { x + y };
};
let addTwo = newAdder(2);
addTwo(2);`
testIntegerObject(t, testEval(input), 4)
}
测试通过。是的:
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> let newAdder = fn(x) { fn(y) { x + y } };
>> let addTwo = newAdder(2);
>> addTwo(3);
5
>> let addThree = newAdder(3);
>> addThree(10);
13
Monkey 有闭包,它们已经在我们的解释器中工作。 多么酷啊? 确切地。 很酷。 但是闭包和最初的问题之间的联系可能还不是很清楚。闭包是“关闭”它们定义环境的函数。它们携带自己的环境,无论何时被调用,它们都可以访问它。两者 上面例子中的重要几行是:
let newAdder = fn(x) { fn(y) { x + y } };
let addTwo = newAdder(2);
这里的 newAdder 是一个高阶函数。 高阶函数是返回其他函数或接收它们作为参数的函数。 在这种情况下,newAdder 返回另一个函数。 但不仅仅是任何函数:一个闭包。 addTwo 绑定到以 2 作为唯一参数调用 newAdder 时返回的闭包。
是什么让 addTwo 成为闭包? 它在调用时可以访问的绑定。
当 addTwo 被调用时,它不仅可以访问调用的参数,即 y 参数,而且还可以访问在 newAdder(2) 调用时绑定到的值 x,即使该绑定很长时间不在 范围并且不再存在于当前环境中:
>> let newAdder = fn(x) { fn(y) { x + y } };
>> let addTwo = newAdder(2);
>>
ERROR: identifier not found:
x 不绑定到我们顶级环境中的值。 但是 addTwo 仍然可以访问它:
>> addTwo(3);
5
换句话说:闭包 addTwo 仍然可以访问在其定义时为当前环境的环境。这是评估 newAdder 主体的最后一行的时间。最后一行是函数字面量。请记住:当评估函数文字时,我们构建一个 object.Function 并在其 .Env 字段中保留对当前环境的引用。
当我们稍后评估 addTwo 的主体时,我们不会在当前环境中评估它,而是在函数的环境中评估它。我们通过扩展函数的环境并将其传递给 Eval 而不是当前环境来做到这一点。为什么?所以它仍然可以访问它。为什么?所以我们可以使用闭包。为什么?因为他们太棒了,我爱他们!
既然我们在谈论令人惊奇的事情,值得一提的是,我们不仅支持从其他函数返回函数,还支持在函数调用中接受函数作为参数。是的,函数在 Monkey 中是一等公民,我们可以像传递任何其他值一样传递它们:
>> let add = fn(a, b) { a + b };
>> let sub = fn(a, b) { a - b };
>> let applyFunc = fn(a, b, func) { func(a, b) };
>> applyFunc(2, 2, add);
4
>> applyFunc(10, 2, sub);
8
这里我们将 add 和 sub 函数作为参数传递给 applyFunc。 然后 applyFunc 毫无问题地调用这个函数:func 参数解析为函数对象,然后使用两个参数调用该函数对象。 没有更多了,一切都已经在我们的解释器中工作了。
我知道您现在在想什么,这是您要发送的消息的模板:
亲爱的 NAME_OF_FRIEND,还记得我说过有一天我会成为一个人并做一些伟大的事情人们会记住我吗? 嗯,今天是一天。 我的 Monkey 解释器可以工作,它支持函数、高阶函数、闭包和整数以及算术和长话短说:我的生活从未如此快乐!
我们做到了。 我们构建了一个完整的 Monkey 解释器,支持函数和函数调用、高阶函数和闭包。 加油,庆祝! 我会在这里等的。
< 3.9绑定和环境 | > 3.11谁来倒垃圾? |
---|