Skip to content

Latest commit

 

History

History
886 lines (775 loc) · 30.2 KB

File metadata and controls

886 lines (775 loc) · 30.2 KB

4.4数组

我们将在本节中添加到 Monkey 解释器的数据类型是数组。 在 Monkey 中,数组是可能不同类型元素的有序列表。 数组中的每个元素都可以单独访问。 数组是通过使用它们的字面形式构造的:逗号分隔的元素列表,用括号括起来。

初始化一个新数组,绑定给它一个名字并且通过独立的元素将会是这样:

>> let myArray = ["Thorsten", "Ball", 28, fn(x) { x * x }];
>> myArray[0]
Thorsten
>> myArray[2]
28
>> myArray[3](2);
4

正如你所见,Monkey数组的确不关心它们元素的类型。Monkey 中每个可能的值都可以是数组中的一个元素。 在这个例子中,myArray 包含两个字符串,一个整数和一个函数。

如最后三行所示,通过数组中的索引访问单个元素是通过一个新的运算符完成的,称为索引运算符:array[index]

在本节中,我们还将为新添加的 len 函数添加对数组的支持,并添加更多用于数组的内置函数:

>> let myArray = ["one", "two", "three"];
>> len(myArray)
3
>> first(myArray)
one
>> rest(myArray)
[two, three]
>> last(myArray)
three
>> push(myArray, "four")
[one, two, three, four]

我们在解释器中实现 Monkey 数组的基础是一个 []object.Object 类型的 Go 切片。 这意味着我们不必实现新的数据结构。 我们可以重用 Go 的切片。

听起来真棒?好的!我们要做的第一件事是教我们的词法分析器一些新的标记。

在我们的词法分析器中支持数组

为了正确解析数组文字和索引运算符,我们的词法分析器需要能够识别比当前更多的标记。 在 Monkey 中构造和使用数组所需的所有标记是 [,] 和 ,。 词法分析器已经知道了,所以我们只需要添加对 [ and ] 的支持。

第一步是在token包中定义这些新的token类型:

// token/token.go

const (
// [...]
    LBRACKET = "["
    RBRACKET = "]"
// [...]
)

第二步是扩展词法分析器的测试套件,这很容易,因为我们之前已经做过很多次了:

// lexer/lexer_test.go、

func TestNextToken(t *testing.T) {
    input := `let five = 5;
let ten = 10;

let add = fn(x, y) {
x + y;
};

let result = add(five, ten);
!-/*5;
5 < 10 > 5;

if (5 < 10) {
return true;
} else {
return false;
}

10 == 10;
10 != 9;
"foobar"
"foo bar"
[1, 2];
`
tests := []struct {
    expectedType token.TokenType
    expectedLiteral string
}{
// [...]
    {token.LBRACKET, "["},
    {token.INT, "1"},
    {token.COMMA, ","},
    {token.INT, "2"},
    {token.RBRACKET, "]"},
    {token.SEMICOLON, ";"},
    {token.EOF, ""},
}
// [...]
}

输入再次扩展为包括新标记(在本例中为 [1, 2]),并添加了新测试以确保词法分析器的 NextToken 方法确实返回 token.LBRACKET 和 ken.RBRACKET。

让测试通过就像将这四行添加到我们的 NextToken() 方法一样简单。 是的,只有四个:

// lexer/lexer.go

func (l *Lexer) NextToken() token.Token {
// [...]
    case '[':
        tok = newToken(token.LBRACKET, l.ch)
    case ']':
        tok = newToken(token.RBRACKET, l.ch)
// [...]
}

好吧!测试通过了:

$ go test ./lexer
ok monkey/lexer 0.006s

在我们的解析器中,我们现在将使用 token.LBRACKET 和 token.RBRACKET 来解析数组。

解析数组文字

正如我们之前看到的,Monkey 中的数组文字是一个逗号分隔的表达式列表,由一个左括号和一个右括号括起来。

[1, 2, 3 + 3, fn(x) { x }, add(2, 2)]

是的,数组文字中的元素可以是任何类型的表达式。 整数文字、函数文字、中缀或前缀表达式。

如果这听起来很复杂,请不要担心。 我们已经知道如何解析逗号分隔的表达式列表——函数调用参数就是这样。 而且我们还知道如何解析由匹配标记包围的内容。 换句话说:让我们开始吧!

我们要做的第一件事是为数组文字定义 AST 节点。 由于我们已经为此准备了必要的部分,因此定义是不言自明的:

// ast/ast.go

type ArrayLiteral struct {
    Token token.Token // the '[' token
    Elements []Expression
}
func (al *ArrayLiteral) expressionNode() {}
func (al *ArrayLiteral) TokenLiteral() string { return al.Token.Literal }
func (al *ArrayLiteral) String() string {
    var out bytes.Buffer

    elements := []string{}
    for _, el := range al.Elements {
        elements = append(elements, el.String())
    }

    out.WriteString("[")
    out.WriteString(strings.Join(elements, ", "))
    out.WriteString("]")

    return out.String()
}

以下测试函数确保解析数组文字会导致返回 *ast.ArrayLiteral。 (我还为空数组文字添加了一个测试函数,以确保我们不会遇到令人讨厌的边缘情况)

// parser/parser_test.go

func TestParsingArrayLiterals(t *testing.T) {
    input := "[1, 2 * 2, 3 + 3]"

    l := lexer.New(input)
    p := New(l)
    program := p.ParseProgram()
    checkParserErrors(t, p)

    stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
    array, ok := stmt.Expression.(*ast.ArrayLiteral)
    if !ok {
        t.Fatalf("exp not ast.ArrayLiteral. got=%T", stmt.Expression)
    }

    if len(array.Elements) != 3 {
        t.Fatalf("len(array.Elements) not 3. got=%d", len(array.Elements))
    }

    testIntegerLiteral(t, array.Elements[0], 1)
    testInfixExpression(t, array.Elements[1], 2, "*", 2)
    testInfixExpression(t, array.Elements[2], 3, "+", 3)
}

只是为了确保表达式的解析确实有效,测试输入包含两个不同的中缀运算符表达式,即使整数或布尔文字就足够了。除此之外,该测试非常无聊,并断言解析器确实返回了具有正确元素数量的 *ast.ArrayLiteral。

为了让测试通过,我们需要在我们的解析器中注册一个新的 prefixParseFn,因为数组文字的开头 token.LBRACKET 位于前缀位置。

// parser/parser.go

func New(l *lexer.Lexer) *Parser {
// [...]
    p.registerPrefix(token.LBRACKET, p.parseArrayLiteral)

// [...]
}

func (p *Parser) parseArrayLiteral() ast.Expression {
    array := &ast.ArrayLiteral{Token: p.curToken}

    array.Elements = p.parseExpressionList(token.RBRACKET)
    
    return array
}

我们之前添加了 prefixParseFns,所以这部分并不是很令人兴奋。 这里有趣的是名为 parseExpressionList 的新方法。 此方法是 parseCallArguments 的修改和通用版本,我们之前在parseCallExpression 中使用它来解析逗号分隔的参数列表:

// parser/parser.go
func (p *Parser) parseExpressionList(end token.TokenType) []ast.Expression {
    list := []ast.Expression{}

    if p.peekTokenIs(end) {
        p.nextToken()
        return list
    }

    p.nextToken()
    list = append(list, p.parseExpression(LOWEST))
    
    for p.peekTokenIs(token.COMMA) {
        p.nextToken()
        p.nextToken()
        list = append(list, p.parseExpression(LOWEST))
    }
    
    if !p.expectPeek(end) {
        return nil
    }

    return list
}

同样,我们之前已经在名称 parseCallArguments 下看到了这一点。 唯一的变化是这个新版本现在接受一个结束参数,它告诉方法哪个标记表示列表的结尾。 更新后的 parseCallExpression 方法,我们之前在其中使用了 parseCallArguments,现在看起来像这样:

// parser/parser.go

func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression {
    exp := &ast.CallExpression{Token: p.curToken, Function: function}
    exp.Arguments = p.parseExpressionList(token.RPAREN)
    return exp
}

唯一的变化是使用 token.RPAREN 调用 parseExpressionList(表示参数列表的结尾)。 我们可以通过更改几行来重用一个相对较大的方法。 很好! 最好的? 测试通过:

$ go test ./parser
ok monkey/parser 0.007s

我们可以将“解析数组文字”标记为“完成”。

解析索引运算符表达式

为了在 Monkey 中完全支持数组,我们不仅需要能够解析数组文字,还需要能够解析索引运算符表达式。 也许“索引操作符”这个名字并不响亮,但我打赌你知道它是什么。 索引运算符表达式如下所示:

myArray[0];
myArray[1];
myArray[2];

至少这是基本形式,但还有很多。 查看这些示例以发现它们背后的结构:

[1, 2, 3, 4][2];

let myArray = [1, 2, 3, 4];
myArray[2];

myArray[2 + 1];

returnsArray()[1];

是的,你完全正确! 基本结构是这样的:

<expression>[<expression>]

这似乎很简单。 我们可以定义一个新的 AST 节点,称为 ast.IndexExpression,它反映了这个结构:

// ast/ast.go

type IndexExpression struct {
    Token token.Token // The [ token
    Left Expression
    Index Expression
}

func (ie *IndexExpression) expressionNode() {}
func (ie *IndexExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *IndexExpression) String() string {
    var out bytes.Buffer

    out.WriteString("(")
    out.WriteString(ie.Left.String())
    out.WriteString("[")
    out.WriteString(ie.Index.String())
    out.WriteString("])")

    return out.String()
}

需要注意的是,Left 和 Index 都只是表达式。 左边是被访问的对象,我们已经看到它可以是任何类型:标识符、数组文字、函数调用。 索引也是如此。 它可以是任何表达式。 从语法上讲,它是哪个没有区别,但从语义上讲,它必须产生一个整数。

Left 和 Index 都是表达式这一事实使解析过程更容易,因为我们可以使用 parseExpression 方法来解析它们。 但首先要注意! 这是确保我们的解析器知道如何返回 *ast.IndexExpression 的测试用例:

// parser/parser_test.go
func TestParsingIndexExpressions(t *testing.T) {
    input := "myArray[1 + 1]"

    l := lexer.New(input)
    p := New(l)
    program := p.ParseProgram()
    checkParserErrors(t, p)

    stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
    indexExp, ok := stmt.Expression.(*ast.IndexExpression)
    if !ok {
        t.Fatalf("exp not *ast.IndexExpression. got=%T", stmt.Expression)
    }

    if !testIdentifier(t, indexExp.Left, "myArray") {
        return
    }

    if !testInfixExpression(t, indexExp.Index, 1, "+", 1) {
        return
    }
}

现在,此测试仅断言解析器为包含索引表达式的单个表达式语句输出正确的 AST。 但同样重要的是解析器正确处理索引运算符的优先级。 索引运算符必须在所有运算符中具有最高优先级。 确保这一点就像扩展我们现有的 Test OperatorPrecedenceParsing 测试函数一样简单:

// parser/parser_test.go
func TestOperatorPrecedenceParsing(t *testing.T) {
    tests := []struct {
        input string
        expected string
    }{
// [...]
        {
        "a * [1, 2, 3, 4][b * c] * d",
        "((a * ([1, 2, 3, 4][(b * c)])) * d)",
        },
        {
        "add(a * b[2], b[1], 2 * [1, 2][1])",
        "add((a * (b[2])), (b[1]), (2 * ([1, 2][1])))",
        },
    }
// [...]
}

*ast.IndexExpression 的 String() 输出中的附加 ( 和 ) 在编写这些测试时帮助我们,因为它们使索引运算符的优先级可见。 在这些添加的测试用例中,我们期望索引运算符的优先级高于调用表达式甚至中缀表达式中的 * 运算符的优先级。

测试失败是因为解析器对索引表达式一无所知:

$ go test ./parser
--- FAIL: TestOperatorPrecedenceParsing (0.00s)
    parser_test.go:393: expected="((a * ([1, 2, 3, 4][(b * c)])) * d)",\
    got="(a * [1, 2, 3, 4])([(b * c)] * d)"
    parser_test.go:968: parser has 4 errors
    parser_test.go:970: parser error: "expected next token to be ), got [ instead"
    parser_test.go:970: parser error: "no prefix parse function for , found"
    parser_test.go:970: parser error: "no prefix parse function for , found"
    parser_test.go:970: parser error: "no prefix parse function for ) found"
    --- FAIL: TestParsingIndexExpressions (0.00s)
    parser_test.go:835: exp not *ast.IndexExpression. got=*ast.Identifier
FAIL
FAIL monkey/parser 0.007s

即使测试抱怨缺少 prefixParseFn,我们想要的是 infixParseFn。 是的,索引运算符表达式在每一侧的操作数之间并没有真正的单个运算符。 但是为了不费吹灰之力地解析它们,像它们一样行事是有好处的,就像我们对调用表达式所做的那样。 具体来说,这意味着将 myArray[0] 中的 [ 视为中缀运算符,将 myArray 视为左操作数,将 0 视为右操作数。

这样做使实现非常适合我们的解析器:

// parser/parser.go

func New(l *lexer.Lexer) *Parser {
// [...]

    p.registerInfix(token.LBRACKET, p.parseIndexExpression)

// [...]
}

func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression {
    exp := &ast.IndexExpression{Token: p.curToken, Left: left}

    p.nextToken()

    exp.Index = p.parseExpression(LOWEST)

    if !p.expectPeek(token.RBRACKET) {
        return nil
    }
    return exp
}

很整洁! 但这并不能解决我们的测试:

$ go test ./parser
--- FAIL: TestOperatorPrecedenceParsing (0.00s)
    parser_test.go:393: expected="((a * ([1, 2, 3, 4][(b * c)])) * d)",\
    got="(a * [1, 2, 3, 4])([(b * c)] * d)"
    parser_test.go:968: parser has 4 errors
    parser_test.go:970: parser error: "expected next token to be ), got [ instead"
    parser_test.go:970: parser error: "no prefix parse function for , found"
    parser_test.go:970: parser error: "no prefix parse function for , found"
    parser_test.go:970: parser error: "no prefix parse function for ) found"
    --- FAIL: TestParsingIndexExpressions (0.00s)
    parser_test.go:835: exp not *ast.IndexExpression. got=*ast.Identifier
FAIL
FAIL monkey/parser 0.008s

那是因为我们的 Pratt 解析器背后的整个想法取决于优先级的想法,而我们还没有定义索引运算符的优先级:

// parser/parser.go

const (
    _ int = iota
// [...]
    INDEX // array[index]
)

var precedences = map[token.TokenType]int{
// [...]
    token.LBRACKET: INDEX,
}

INDEX 的定义是 const 块中的最后一行,这一点很重要。 由于 iota,这使 INDEX 成为所有定义的优先级常量的最高值。 在 precedences 中添加的条目为 token.LBRACKET 提供了最高的优先级,INDEX。 而且,它确实有奇效:

$ go test ./parser
ok monkey/parser 0.007s

词法分析器完成,解析器完成。 评估区见!

评估数组文字

评估数组文字并不难。 将 Monkey 数组映射到 Go 的切片让生活变得美好、甜蜜。 我们不必实现新的数据结构。 我们只需要定义一个新的 object.Array 类型,因为这就是数组字面量的计算结果。 而 object.Array 的定义很简单,因为 Monkey 中的数组很简单:它们只是一个对象列表。

// object/object.go

const (
// [...]
    ARRAY_OBJ = "ARRAY"
)

type Array struct {
    Elements []Object
}

func (ao *Array) Type() ObjectType { return ARRAY_OBJ }
func (ao *Array) Inspect() string {
    var out bytes.Buffer

    elements := []string{}
    for _, e := range ao.Elements {
        elements = append(elements, e.Inspect())
    }

    out.WriteString("[")
    out.WriteString(strings.Join(elements, ", "))
    out.WriteString("]")

    return out.String()
}

当我说这个定义最复杂的地方是 Inspect 方法时,我想你会同意我的观点。 即使是那个也很容易理解。

这是数组文字的评估器测试:

// evaluator/evaluator_test.go

func TestArrayLiterals(t *testing.T) {
    input := "[1, 2 * 2, 3 + 3]"

    evaluated := testEval(input)
    result, ok := evaluated.(*object.Array)
    if !ok {
        t.Fatalf("object is not Array. got=%T (%+v)", evaluated, evaluated)
    }

    if len(result.Elements) != 3 {
        t.Fatalf("array has wrong num of elements. got=%d",
        len(result.Elements))
    }

    testIntegerObject(t, result.Elements[0], 1)
    testIntegerObject(t, result.Elements[1], 4)
    testIntegerObject(t, result.Elements[2], 6)
}

我们可以重用一些现有的代码来让这个测试通过,就像我们在解析器中所做的那样。 同样,我们重用的代码最初是为调用表达式编写的。 这是评估 *ast.ArrayLiterals 并生成数组对象的 case 分支:

// evaluator/evaluator.go

func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]

    case *ast.ArrayLiteral:
        elements := evalExpressions(node.Elements, env)
        if len(elements) == 1 && isError(elements[0]) {
            return elements[0]
        }
        return &object.Array{Elements: elements}
    }
    
// [...]
}

这难道不是编程的一大乐趣吗? 重用现有代码,而不必将其变成超级通用、过度设计的宇宙飞船。 测试通过,我们可以在 REPL 中使用数组文字来生成数组:

$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> [1, 2, 3, 4]
[1, 2, 3, 4]
>> let double = fn(x) { x * 2 };
>> [1, double(2), 3 * 3, 4 - 3]
[1, 4, 9, 1]
>>

很神奇,不是吗? 但是我们还不能做的是使用索引运算符访问数组的单个元素。

评估索引运算符表达式

好消息:比评估索引表达式更难的是解析它们。 我们已经这样做了。 剩下的唯一问题是访问和检索数组中的元素时可能会出现一对一错误。 但为此,我们只需在我们的测试套件中添加一些测试:

// evaluator/evaluator_test.go

func TestArrayIndexExpressions(t *testing.T) {
    tests := []struct {
        input string
        expected interface{}
    }{
        {
        "[1, 2, 3][0]",
        1,
        },
        {
        "[1, 2, 3][1]",
        2,
        },
        {
        "[1, 2, 3][2]",
        3,
        },
        {
        "let i = 0; [1][i];",
        1,
        },
        {
        "[1, 2, 3][1 + 1];",
        3,
        },
        {
        "let myArray = [1, 2, 3]; myArray[2];",
        3,
        },
        {
        "let myArray = [1, 2, 3]; myArray[0] + myArray[1] + myArray[2];",
        6,
        },
        {
        "let myArray = [1, 2, 3]; let i = myArray[0]; myArray[i]",
        2,
        },
        {
        "[1, 2, 3][3]",
        nil,
        },
        {
        "[1, 2, 3][-1]",
        nil,
        },
    }

    for _, tt := range tests {
        evaluated := testEval(tt.input)
        integer, ok := tt.expected.(int)
        if ok {
            testIntegerObject(t, evaluated, int64(integer))
        } else {
            testNullObject(t, evaluated)
        }
    }
}

好吧,我承认,这些测试可能看起来有些过分。 我们在这里隐式测试的很多东西已经在其他地方测试过了。 但是测试用例是那么容易写! 他们是如此易读! 我喜欢这些测试。

记下这些测试指定的所需行为。 它们包含一些我们尚未讨论的内容:当我们使用超出数组边界的索引时,我们将返回 NULL。 在这种情况下,某些语言会产生错误,而某些语言会返回空值。 我选择返回NULL。

正如预期的那样,测试失败了。 不仅如此,它们还在爆炸:

$ go test ./evaluator
--- FAIL: TestArrayIndexExpressions (0.00s)
    evaluator_test.go:492: object is not Integer. got=<nil> (<nil>)
    evaluator_test.go:492: object is not Integer. got=<nil> (<nil>)
    evaluator_test.go:492: object is not Integer. got=<nil> (<nil>)
    evaluator_test.go:492: object is not Integer. got=<nil> (<nil>)
    evaluator_test.go:492: object is not Integer. got=<nil> (<nil>)
    evaluator_test.go:492: object is not Integer. got=<nil> (<nil>)
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x70057]
[redacted: backtrace here]
FAIL monkey/evaluator 0.011s

那么我们如何解决这个问题并评估索引表达式呢? 正如我们所见,索引运算符的左操作数可以是任何表达式,而索引本身可以是任何表达式。 这意味着我们需要先评估两者,然后才能评估“索引”本身。 否则,我们会尝试访问标识符或函数调用的元素,但这是行不通的。

这是 *ast.IndexExpression 的 case 分支,它对 Eval 进行了这些所需的调用:

// evaluator/evaluator.go

func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
    case *ast.IndexExpression:
        left := Eval(node.Left, env)
        if isError(left) {
            return left
        }
        index := Eval(node.Index, env)
        if isError(index) {
            return index
        }
        return evalIndexExpression(left, index)

// [...]
}

这是它使用的 evalIndexExpression 函数:

// evaluator/evaluator.go

func evalIndexExpression(left, index object.Object) object.Object {
    switch {
    case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ:
        return evalArrayIndexExpression(left, index)
    default:
        return newError("index operator not supported: %s", left.Type())
    }
}

if 条件在这里可以很好地完成 switch 语句的工作,但我们将在本章稍后添加另一个 case 分支。 除了错误处理(为此我还添加了一个测试),这个函数没有任何有趣的事情发生。 操作的核心在evalArrayIndexExpression 中:

// evaluator/evaluator.go

func evalArrayIndexExpression(array, index object.Object) object.Object {
    arrayObject := array.(*object.Array)
    idx := index.(*object.Integer).Value
    max := int64(len(arrayObject.Elements) - 1)

    if idx < 0 || idx > max {
        return NULL
    }

    return arrayObject.Elements[idx]
}

这里我们实际上从数组中检索具有指定索引的元素。 除了小类型断言和转换舞蹈之外,这个函数非常简单:它检查给定的索引是否超出范围,如果是这种情况,则返回 NULL,否则返回所需的元素。 就像我们在测试中指定的那样,现在正在通过:

$ 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 a = [1, 2 * 2, 10 - 5, 8 / 2];
>> a[0]
1
>> a[1]
4
>> a[5 - 3]
5
>> a[99]
null

从数组中检索元素有效! 甜的! 我只能在这里重复一遍:实现这个语言特性是多么容易,不是吗?

为数组添加内置函数

我们现在可以使用数组字面量来构造数组。 我们可以使用索引表达式访问单个元素。 仅这两件事就使数组非常有用。 但为了让它们更有用,我们需要添加一些内置函数,使使用它们更方便。 在本小节中,我们将完全做到这一点。

我不会在本节中展示任何测试代码和测试用例。 原因是这些特定的测试占用了空间而没有添加任何新内容。 我们用于测试内置函数的“框架”已经与 TestBuiltinFunctions 一起就位,并且添加的测试遵循现有方案。 您可以在随附的代码中找到它们。

我们的目标是添加新的内置函数。 但是我们实际上要做的第一件事不是添加一个新的,而是改变一个现有的功能。 我们需要为 len 添加对数组的支持,它目前只支持字符串:

// evaluator/builtins.go
var builtins = map[string]*object.Builtin{
    "len": &object.Builtin{
        Fn: func(args ...object.Object) object.Object {
        if len(args) != 1 {
            return newError("wrong number of arguments. got=%d, want=1",
                len(args))
            }

        switch arg := args[0].(type) {
            case *object.Array:
                return &object.Integer{Value: int64(len(arg.Elements))}
            case *object.String:
                return &object.Integer{Value: int64(len(arg.Value))}
            default:
                return newError("argument to `len` not supported, got %s",
                    args[0].Type())
            }
        },
    },
// [...]
}

唯一的变化是为 *object.Array 添加了 case 分支。 有了这些,我们就可以开始添加新功能了。 耶!

这些新的内置函数中的第一个是第一个。 首先返回给定数组的第一个元素。 是的,调用 myArray[0] 做同样的事情。 但可以说第一个更漂亮。 这是它的实现:

// evaluator/builtins.go

var builtins = map[string]*object.Builtin{
// [...]

    "first": &object.Builtin{
        Fn: func(args ...object.Object) object.Object {
        if len(args) != 1 {
            return newError("wrong number of arguments. got=%d, want=1",
                len(args))
        }
        if args[0].Type() != object.ARRAY_OBJ {
            return newError("argument to `first` must be ARRAY, got %s",
                args[0].Type())
        }

        arr := args[0].(*object.Array)
        if len(arr.Elements) > 0 {
            return arr.Elements[0]
        }

            return NULL
        },
    },
}

伟大的! 这样可行! 首先是什么? 你是对的,我们要添加的下一个函数是 last 调用的。 last 的目的是返回给定数组的最后一个元素。 在索引运算符方面,它返回 myArray[len(myArray)-1]。 事实证明,最后实施并不比先实施困难多少——谁会想到这一点? 这里是:

// evaluator/builtins.go

var builtins = map[string]*object.Builtin{
// [...]

    "last": &object.Builtin{
        Fn: func(args ...object.Object) object.Object {
        if len(args) != 1 {
            return newError("wrong number of arguments. got=%d, want=1",
                len(args))
            }
        if args[0].Type() != object.ARRAY_OBJ {
            return newError("argument to `last` must be ARRAY, got %s",
                args[0].Type())
            }

            arr := args[0].(*object.Array)
            length := len(arr.Elements)
            if length > 0 {
                return arr.Elements[length-1]
            }

            return NULL
        },
    },
}

我们要添加的下一个函数在 Scheme 中称为 cdr。 在其他一些语言中,它有时被称为尾部。 我们将称之为休息。 rest 返回一个新数组,其中包含作为参数传递的数组的所有元素,但第一个除外。 这是使用它的样子:

>> let a = [1, 2, 3, 4];
>> rest(a)
[2, 3, 4]
>> rest(rest(a))
[3, 4]
>> rest(rest(rest(a)))
[4]
>> rest(rest(rest(rest(a))))
[]
>> rest(rest(rest(rest(rest(a)))))
null

它的实现很简单,但请记住,我们正在返回一个新分配的数组。 我们不会修改传递给 rest 的数组:

// evaluator/builtins.go

var builtins = map[string]*object.Builtin{
// [...]

    "rest": &object.Builtin{
        Fn: func(args ...object.Object) object.Object {
        if len(args) != 1 {
            return newError("wrong number of arguments. got=%d, want=1",
                len(args))
        }

        if args[0].Type() != object.ARRAY_OBJ {
            return newError("argument to `rest` must be     ARRAY, got %s",
                args[0].Type())
        }

        arr := args[0].(*object.Array)
        length := len(arr.Elements)
        if length > 0 {
            newElements := make([]object.Object, length-1, length-1)
            copy(newElements, arr.Elements[1:length])
            return &object.Array{Elements: newElements}
        }

        return NULL
        },
    },
}

我们将要构建到解释器中的最后一个数组函数称为 push。 它将一个新元素添加到数组的末尾。 但是,关键是,它不会修改给定的数组。 相反,它分配一个新数组,该数组具有与旧数组相同的元素加上新的推送元素。 数组在 Monkey 中是不可变的。 这是推动行动:

>> let a = [1, 2, 3, 4];
>> let b = push(a, 5);
>> a
[1, 2, 3, 4]
>> b
[1, 2, 3, 4, 5]

并且这里是它的实现:

// evaluator/builtins.go

var builtins = map[string]*object.Builtin{
// [...]

    "push": &object.Builtin{
        Fn: func(args ...object.Object) object.Object {
            if len(args) != 2 {
                return newError("wrong number of arguments. got=%d, want=2",
                    len(args))
            }

            if args[0].Type() != object.ARRAY_OBJ {
                return newError("argument to `push` must be ARRAY, got %s",
                    args[0].Type())
            }

            arr := args[0].(*object.Array)
            length := len(arr.Elements)

            newElements := make([]object.Object, length+1, length+1)
            copy(newElements, arr.Elements)
            newElements[length] = args[1]

            return &object.Array{Elements: newElements}
        },
    },
}

测试驱动数组

我们现在有了数组字面量、索引运算符和一些处理数组的内置函数。是时候带他们去兜风了。 让我们看看他们能做什么。

首先,rest 和 push 我们可以构建一个地图函数:

let map = fn(arr, f) {
    let iter = fn(arr, accumulated) {
    if (len(arr) == 0) {
        accumulated
    } else {
        iter(rest(arr), push(accumulated, f(first(arr))));
    }
};

iter(arr, []);
};

使用 map 我们可以做这样的事情:

>> let a = [1, 2, 3, 4];
>> let double = fn(x) { x * 2 };
>> map(a, double);
[2, 4, 6, 8]

这不是很神奇吗? 还有更多! 基于相同的内置函数,我们还可以定义一个 reduce 函数:

let reduce = fn(arr, initial, f) {
    let iter = fn(arr, result) {
        if (len(arr) == 0) {
            result
        } else {
            iter(rest(arr), f(result, first(arr)));
        }
    };

    iter(arr, initial);
};

reduce,reduce 可用于定义 sum 函数:

let sum = fn(arr) {
reduce(arr, 0, fn(initial, el) { initial + el });
};

它的作用就像一个魅力:

>> sum([1, 2, 3, 4, 5]);
15

你可能知道,我不喜欢拍拍自己的背,但我只想说:holy monkey! 看看我们的解释器能做什么! map功能?! reduce?! 我们已经走了很长很长的路!

而且这还不是全部! 我们现在可以做的还有很多,我敦促您探索数组数据类型和少数内置函数为我们提供的可能性。 但是你知道你应该先做什么吗? 休息一段时间,向你的朋友和家人吹嘘这件事,享受赞美和赞美。 当你回来时,我们将添加另一种数据类型。

< 内置函数 > 4.5哈希