我们要添加的下一个数据类型称为“哈希”。 Monkey 中的哈希有时在其他编程语言中被称为散列、映射、哈希映射或字典。 它将键映射到值。
为了在 Monkey 中构造哈希,可以使用哈希文字:用大括号括起来的以逗号分隔的键值对列表。 每个键值对使用冒号来区分键和值。 这是使用哈希文字的样子。
>> let myHash = {"name": "Jimmy", "age": 72, "band": "Led Zeppelin"};
>> myHash["name"]
Jimmy
>> myHash["age"]
72
>> myHash["band"]
Led Zeppelin
在这个例子中,myHash 包含三个键值对。 键都是字符串。 而且,如您所见,我们可以使用索引运算符表达式再次从哈希中获取值,就像我们可以使用数组一样。 除了在这个例子中索引值是字符串,它不适用于数组。 这甚至不是唯一可用作哈希键的数据类型:
>> let myHash = {true: "yes, a boolean", 99: "correct, an integer"};
>> myHash[true]
yes, a boolean
>> myHash[99]
correct, an integer
这也是有效的。事实上,除了字符串,整数和布尔值,我们还可以使用任何表达式作为索引运算符表达式中的索引:
>> myHash[5 > 1]
yes, a boolean
>> myHash[100 - 1]
correct, an integer
只要这些表达式的计算结果为字符串、整数或布尔值,它们就可以用作哈希键。 这里 5 > 1 的计算结果为 true , 100 - 1 的计算结果为 99,它们都是有效的并映射到 myHash 中的值。
不出所料,我们的实现将使用 Go 的 map 作为 Monkey 哈希的底层数据结构。 但是由于我们想要交替使用字符串、整数和布尔值作为键,我们需要在普通的旧映射之上构建一些东西以使其工作。 当我们扩展我们的对象系统时,我们会谈到这一点。 但首先我们必须将哈希文字转换为tokens。
我们如何将哈希文字转换为token? 我们需要在词法分析器中识别和输出哪些标记,以便我们以后可以在解析器中使用它们? 这是上面的哈希文字:
{"name": "Jimmy", "age": 72, "band": "Led Zeppelin"}
除了字符串文字之外,这里还使用了四个重要的字符:{,},和:。 我们已经知道如何对前三个进行词法分析。 我们的词法分析器将它们分别转换为 token.LBRACE、token.RBRACE 和 token.COMMA。 这意味着,我们在本节中要做的就是将 : 变成一个token。 为此,我们首先需要在token包中定义必要的token类型:
// token/token.go
const (
// [...]
COLON = ":"
// [...]
)
接下来,我们将为需要 token.COLON 的词法分析器的 NextToken 方法添加一个新测试:
// 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];
{"foo": "bar"}
`
tests := []struct {
expectedType token.TokenType
expectedLiteral string
}{
// [...]
{token.LBRACE, "{"},
{token.STRING, "foo"},
{token.COLON, ":"},
{token.STRING, "bar"},
{token.RBRACE, "}"},
{token.EOF, ""},
}
// [...]
}
我们可以避免在测试输入中添加一个 : ,但是在稍后阅读和最终调试测试时使用我们在这里所做的哈希文字提供了更多的上下文。
将 : 转换为 token.COLON 非常简单:
// lexer/lexer.go
func (l *Lexer) NextToken() token.Token {
// [...]
case ':':
tok = newToken(token.COLON, l.ch)
// [...]
}
只有两个新行,词法分析器现在吐出 token.COLON:
$ go test ./lexer
ok monkey/lexer 0.006s
Boom!词法分析器现在返回 token.LBRACE、token.RBRACE、token.COMMA 和新的 token.COLON。 这就是我们解析哈希文字所需的全部内容。
在我们开始处理我们的解析器甚至编写测试之前,让我们看看哈希文字的基本语法结构:
{<expression> : <expression>, <expression> : <expression>, ... }
这是一个逗号分隔的对列表。 每对由两个表达式组成。 一个产生哈希键,一个产生值。 键与值之间用冒号分隔。 该列表由一对花括号括起来。
当我们把它变成一个 AST 节点时,我们必须跟踪键值对。 现在我们将如何做到这一点? 是的,我们将使用映射,但是该映射中的键和值是什么类型的?
这是因为很多不同的表达式都可以产生字符串、整数或布尔值。 不仅仅是它们的字面形式。 在解析阶段强制执行哈希键的数据类型会阻止我们做这样的事情:
let key = "name";
let hash = {key: "Monkey"};
这里 key 的计算结果为“name”,因此作为哈希键是完全有效的,即使它是一个标识符。为了实现这一点,我们需要允许任何表达式作为键和任何表达式作为哈希文字中的值。 至少在解析阶段。 接着我们的 ast.HashLiteral 定义如下所示:
// ast/ast.go
type HashLiteral struct {
Token token.Token // the '{' token
Pairs map[Expression]Expression
}
func (hl *HashLiteral) expressionNode() {}
func (hl *HashLiteral) TokenLiteral() string { return hl.Token.Literal }
func (hl *HashLiteral) String() string {
var out bytes.Buffer
pairs := []string{}
for key, value := range hl.Pairs {
pairs = append(pairs, key.String()+":"+value.String())
}
out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")
return out.String()
}
现在我们已经清楚哈希文字的结构并定义了 ast.HashLiteral,我们可以为我们的解析器编写测试:
// parser/parser_test.go
func TestParsingHashLiteralsStringKeys(t *testing.T) {
input := `{"one": 1, "two": 2, "three": 3}`
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
stmt := program.Statements[0].(*ast.ExpressionStatement)
hash, ok := stmt.Expression.(*ast.HashLiteral)
if !ok {
t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression)
}
if len(hash.Pairs) != 3 {
t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs))
}
expected := map[string]int64{
"one": 1,
"two": 2,
"three": 3,
}
for key, value := range hash.Pairs {
literal, ok := key.(*ast.StringLiteral)
if !ok {
t.Errorf("key is not ast.StringLiteral. got=%T", key)
}
expectedValue := expected[literal.String()]
testIntegerLiteral(t, value, expectedValue)
}
}
当然,我们还必须确保正确解析空哈希文字,因为这种边缘情况是编程中所有脱发的根源:
// parser/parser_test.go
func TestParsingEmptyHashLiteral(t *testing.T) {
input := "{}"
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
stmt := program.Statements[0].(*ast.ExpressionStatement)
hash, ok := stmt.Expression.(*ast.HashLiteral)
if !ok {
t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression)
}
if len(hash.Pairs) != 0 {
t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs))
}
}
我还添加了两个类似于 TestHashLiteralStringKeys 的测试,但使用整数和布尔值作为哈希键,并确保解析器将它们分别转换为 *ast.IntegerLiteral 和 *ast.Boolean。 然后是第五个测试函数,它确保哈希文字中的值可以是任何表达式,甚至是运算符表达式。 它看起来像这样:
// parser/parser_test.go
func TestParsingHashLiteralsWithExpressions(t *testing.T) {
input := `{"one": 0 + 1, "two": 10 - 8, "three": 15 / 5}`
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
stmt := program.Statements[0].(*ast.ExpressionStatement)
hash, ok := stmt.Expression.(*ast.HashLiteral)
if !ok {
t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression)
}
if len(hash.Pairs) != 3 {
t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs))
}
tests := map[string]func(ast.Expression){
"one": func(e ast.Expression) {
testInfixExpression(t, e, 0, "+", 1)
},
"two": func(e ast.Expression) {
testInfixExpression(t, e, 10, "-", 8)
},
"three": func(e ast.Expression) {
testInfixExpression(t, e, 15, "/", 5)
},
}
for key, value := range hash.Pairs {
literal, ok := key.(*ast.StringLiteral)
if !ok {
t.Errorf("key is not ast.StringLiteral. got=%T", key)
continue
}
testFunc, ok := tests[literal.String()]
if !ok {
t.Errorf("No test function for key %q found", literal.String())
continue
}
testFunc(value)
}
}
那么所有这些测试功能的表现如何呢? 不那么好,说实话。 我们遇到了很多失败和解析器错误:
$ go test ./parser
--- FAIL: TestParsingEmptyHashLiteral (0.00s)
parser_test.go:1173: parser has 2 errors
parser_test.go:1175: parser error: "no prefix parse function for { found"
parser_test.go:1175: parser error: "no prefix parse function for } found"
--- FAIL: TestParsingHashLiteralsStringKeys (0.00s)
parser_test.go:1173: parser has 7 errors
parser_test.go:1175: parser error: "no prefix parse function for { found"
[... more errors ...]
--- FAIL: TestParsingHashLiteralsBooleanKeys (0.00s)
parser_test.go:1173: parser has 5 errors
parser_test.go:1175: parser error: "no prefix parse function for { found"
[... more errors ...]
--- FAIL: TestParsingHashLiteralsIntegerKeys (0.00s)
parser_test.go:967: parser has 7 errors
parser_test.go:969: parser error: "no prefix parse function for { found"
[... more errors ...]
--- FAIL: TestParsingHashLiteralsWithExpressions (0.00s)
parser_test.go:1173: parser has 7 errors
parser_test.go:1175: parser error: "no prefix parse function for { found"
[... more errors ...]
FAIL
FAIL monkey/parser 0.008s
这听起来可能令人难以置信,但有个好消息:只需要一个函数就可以通过所有这些测试。 确切地说,是一个 prefixParseFn。 由于哈希文字的 token.LBRACE 位于前缀位置,就像数组文字的 token.LBRACKET 一样,我们可以将 parseHashLiteral 方法定义为 prefixParseFn:
// parser/parser.go
func New(l *lexer.Lexer) *Parser {
// [...]
p.registerPrefix(token.LBRACE, p.parseHashLiteral)
// [...]
}
func (p *Parser) parseHashLiteral() ast.Expression {
hash := &ast.HashLiteral{Token: p.curToken}
hash.Pairs = make(map[ast.Expression]ast.Expression)
for !p.peekTokenIs(token.RBRACE) {
p.nextToken()
key := p.parseExpression(LOWEST)
if !p.expectPeek(token.COLON) {
return nil
}
p.nextToken()
value := p.parseExpression(LOWEST)
hash.Pairs[key] = value
if !p.peekTokenIs(token.RBRACE) && !p.expectPeek(token.COMMA) {
return nil
}
}
if !p.expectPeek(token.RBRACE) {
return nil
}
return hash
}
它可能看起来很吓人,但 parseHashLiteral 中没有我们以前没见过的东西。 它仅通过检查结束标记.RBRACE 并调用 parseExpression 两次来遍历键值表达式对。 那和 hash.Pairs 的填充是这个方法最重要的部分。 它很好地完成了它的工作:
$ go test ./parser
ok monkey/parser 0.006s
我们所有的解析器测试都通过了! 从我们添加的测试数量来看,我们可以合理地确定我们的解析器现在知道如何解析哈希文字。 这意味着我们现在来到了向解释器添加哈希的最有趣的部分:在对象系统中表示它们并评估哈希文字。
除了扩展词法分析器和解析器之外,添加新的数据类型也意味着在对象系统中表示它。 我们成功地为整数、字符串和数组做到了这一点。 但是,虽然实现这些其他数据类型只是意味着定义一个具有正确类型的 .Value 字段的结构,但哈希需要更多的努力。 让我解释一下原因。 假设我们定义了一个新的 object.Hash 类型,如下所示:
type Hash struct {
Pairs map[Object]Object
}
这是基于 Go 的 map 实现 Hash 数据类型的最明显的选择。 但是有了这个定义,我们将如何填充 Pairs 映射? 更重要的是,我们如何从中获得价值?
考虑这段 Monkey 代码:
let hash = {"name": "Monkey"};
hash["name"]
假设我们正在评估这两行并使用上面的 object.Hash 定义。 在评估第一行中的哈希文字时,我们将每个键值对放入 map[Object]Object 映射中,导致 .Pairs 具有以下映射:一个 *object.String 与 .Value 被"name"映射 到一个 *object.String 与 .Value 是“Monkey”。
到现在为止还挺好。 但是问题出现在我们使用索引表达式尝试访问"Monkey"字符串的第二行。
在第二行中,索引表达式的"name"字符串字面量计算为一个新的、新分配的 *object.String。 即使这个新的 *object.String 在其 .Value 字段中也包含“name”,就像 Pairs 中的另一个 *object.String 一样,我们不能使用新的来检索“Monkey”。
原因是它们是指向不同内存位置的指针。 它们指向的内存位置的内容相同("name")这一事实无关紧要。 比较这些指针会告诉我们它们不相等。 这意味着使用新创建的 *object.String 作为键不会让我们得到"Monkey"。 这就是指针和它们之间的比较在 Go 中的工作方式。
这是一个示例,演示了我们在使用上面的 object.Hash 实现时会遇到的问题:
name1 := &object.String{Value: "name"}
monkey := &object.String{Value: "Monkey"}
pairs := map[object.Object]object.Object{}
pairs[name1] = monkey
fmt.Printf("pairs[name1]=%+v\n", pairs[name1])
// => pairs[name1]=&{Value:Monkey}
name2 := &object.String{Value: "name"}
fmt.Printf("pairs[name2]=%+v\n", pairs[name2])
// => pairs[name2]=<nil>
fmt.Printf("(name1 == name2)=%t\n", name1 == name2)
// => (name1 == name2)=false
作为这个问题的解决方案,我们可以遍历 .Pairs 中的每个键,检查它是否是 *object.String 并将其 .Value 与索引表达式中键的 .Value 进行比较。 我们会通过这种方式找到匹配的值,但是这种方法将给定键的查找时间从 O(1) 变成了 O(n),首先违背了使用哈希的全部目的。
另一种选择是将 Pairs 定义为 map[string]Object,然后使用 *object.String 的 .Value 作为键。 这有效,但不适用于整数和布尔值。
不,我们需要的是一种为对象生成哈希的方法,我们可以轻松地比较这些哈希并将其用作 object.Hash 中的哈希键。 我们需要能够为 *object.String 生成一个哈希键,该哈希键与另一个具有相同 .Value 的 *object.String 的哈希键相当并相等。 *object.Integer 和 *object.Boolean 也是如此。 但是 *object.String 的散列键永远不能 等于 *object.Integer 或 *object.Boolean 的散列键。 在类型之间,哈希键必须始终不同。
我们可以在对象系统中的一组测试函数中表达所需的行为:
// object/object_test.go
package object
import "testing"
func TestStringHashKey(t *testing.T) {
hello1 := &String{Value: "Hello World"}
hello2 := &String{Value: "Hello World"}
diff1 := &String{Value: "My name is johnny"}
diff2 := &String{Value: "My name is johnny"}
if hello1.HashKey() != hello2.HashKey() {
t.Errorf("strings with same content have different hash keys")
}
if diff1.HashKey() != diff2.HashKey() {
t.Errorf("strings with same content have different hash keys")
}
if hello1.HashKey() == diff1.HashKey() {
t.Errorf("strings with different content have same hash keys")
}
}
这正是我们想要的 HashKey() 方法。 不仅针对*object.String,还针对 *object.Boolean 和 *object.Integer,这就是为什么它们也存在相同的测试函数的原因。 为了阻止测试失败,我们需要在三种类型中的每一种上实现 HashKey() 方法:
// object/object.go
import (
// [...]
"hash/fnv"
)
type HashKey struct {
Type ObjectType
Value uint64
}
func (b *Boolean) HashKey() HashKey {
var value uint64
if b.Value {
value = 1
} else {
value = 0
}
return HashKey{Type: b.Type(), Value: value}
}
func (i *Integer) HashKey() HashKey {
return HashKey{Type: i.Type(), Value: uint64(i.Value)}
}
func (s *String) HashKey() HashKey {
h := fnv.New64a()
h.Write([]byte(s.Value))
return HashKey{Type: s.Type(), Value: h.Sum64()}
}
每个 HashKey() 方法都返回一个 HashKey。 正如您在其定义中所见,HashKey 没什么特别的。 Type 字段包含一个 ObjectType,因此有效地将 HashKeys“范围”到不同的对象类型。 Value 字段保存实际的哈希值,它只是一个整数。 由于它只是两个整数,我们可以使用 == 运算符轻松地将一个 HashKey 与另一个 HashKey 进行比较。 这也使 HashKey 可用作 Go 映射中的键。
我们之前演示的问题是通过使用这个新定义的 HashKey 和 HashKey() 方法解决的:
name1 := &object.String{Value: "name"}
monkey := &object.String{Value: "Monkey"}
pairs := map[object.HashKey]object.Object{}
pairs[name1.HashKey()] = monkey
fmt.Printf("pairs[name1.HashKey()]=%+v\n", pairs[name1.HashKey()])
// => pairs[name1.HashKey()]=&{Value:Monkey}
name2 := &object.String{Value: "name"}
fmt.Printf("pairs[name2.HashKey()]=%+v\n", pairs[name2.HashKey()])
// => pairs[name2.HashKey()]=&{Value:Monkey}
fmt.Printf("(name1 == name2)=%t\n", name1 == name2)
// => (name1 == name2)=false
fmt.Printf("(name1.HashKey() == name2.HashKey())=%t\n",
name1.HashKey() == name2.HashKey())
// => (name1.HashKey() == name2.HashKey())=true
这正是我们想要的! HashKey 定义和 HashKey() 方法实现解决了我们在原始 Hash 定义中遇到的问题。 它们还使测试通过:
$ go test ./object
ok monkey/object 0.008s
现在我们可以定义 object.Hash 并使用这个新的 HashKey 类型:
// object/object.go
const (
// [...]
HASH_OBJ = "HASH"
)
type HashPair struct {
Key Object
Value Object
}
type Hash struct {
Pairs map[HashKey]HashPair
}
func (h *Hash) Type() ObjectType { return HASH_OBJ }
这增加了 Hash 和 HashPair 的定义。 HashPair 是 Hash.Pairs 中值的类型。 您可能想知道为什么我们使用它而不只是将 Pairs 定义为 map[HashKey]Object。
原因是Hash的Inspect()方法。 当我们稍后在 REPL 中打印 Monkey 哈希时,我们希望打印哈希中包含的值及其键。 而仅仅打印 HashKeys 并不是很有用。 所以我们通过使用 HashPairs 作为值来跟踪生成 HashKeys 的对象,我们保存原始键对象和它映射到的值对象。 这样我们就可以调用关键对象的 Inspect() 方法来为 *object.Hash 生成 Inspect() 输出。 这里说的是 Inspect() 方法:
// object/object.go
func (h *Hash) Inspect() string {
var out bytes.Buffer
pairs := []string{}
for _, pair := range h.Pairs {
pairs = append(pairs, fmt.Sprintf("%s: %s",
pair.Key.Inspect(), pair.Value.Inspect()))
}
out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")
return out.String()
}
Inspect() 方法并不是跟踪生成 HashKey 的对象的唯一原因。 如果我们要为 Monkey 哈希实现类似范围函数的东西,这也是必要的,它迭代哈希中的键和值。 或者,如果我们想添加一个 firstPair 函数,该函数将给定哈希的第一个键和值作为数组返回。 或者如果我们想要......你明白了。 跟踪键非常有用,尽管目前只有 Inspect() 方法有好处。
就是这样! 这就是 object.Hash 的整个实现。 但是当我们仍然打开object包时,我们应该做一件小事:
// object/object.go
type Hashable interface {
HashKey() HashKey
}
我们可以在我们的评估器中使用这个接口来检查给定对象是否可用作哈希键,当我们评估哈希文字或索引表达式时。 目前仅由 *object.String、 *object.Boolean 和 *object.Integer 实现。 当然,在继续之前我们还可以做一件事:我们可以通过缓存它们的返回值来优化 HashKey() 方法的性能,但这对于注重性能的读者来说听起来是一个很好的练习。
我们即将开始评估哈希文字,我会完全诚实地对你说:向我们的解释器添加哈希最困难的部分已经结束。 从此一帆风顺。 因此,让我们享受旅程,放松并编写测试:
// evaluator/evaluator_test.go
func TestHashLiterals(t *testing.T) {
input := `let two = "two";
{
"one": 10 - 9,
two: 1 + 1,
"thr" + "ee": 6 / 2,
4: 4,
true: 5,
false: 6
}`
evaluated := testEval(input)
result, ok := evaluated.(*object.Hash)
if !ok {
t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated)
}
expected := map[object.HashKey]int64{
(&object.String{Value: "one"}).HashKey(): 1,
(&object.String{Value: "two"}).HashKey(): 2,
(&object.String{Value: "three"}).HashKey(): 3,
(&object.Integer{Value: 4}).HashKey(): 4,
TRUE.HashKey(): 5,
FALSE.HashKey(): 6,
}
if len(result.Pairs) != len(expected) {
t.Fatalf("Hash has wrong num of pairs. got=%d", len(result.Pairs))
}
for expectedKey, expectedValue := range expected {
pair, ok := result.Pairs[expectedKey]
if !ok {
t.Errorf("no pair for given key in Pairs")
}
testIntegerObject(t, pair.Value, expectedValue)
}
}
这个测试函数显示了当 Eval 遇到 *ast.HashLiteral 时我们想要什么:一个新的 *object.Hash,其正确数量的 HashPairs 映射到其 Pairs 属性中匹配的 HashKeys。
它还显示了我们的另一个要求:字符串、标识符、中缀运算符表达式、布尔值和整数——它们都应该可以用作键。 真的任何表情。 只要它产生一个实现 Hashable 接口的对象,它就应该可以用作哈希键。 然后是价值观。 它们也可以由任何表达式产生。 我们在这里通过断言 10 - 9 的计算结果为 1、6 / 2 到 3 等等来对此进行测试。
测试如期失败:
$ go test ./evaluator
--- FAIL: TestHashLiterals (0.00s)
evaluator_test.go:522: Eval didn't return Hash. got=<nil> (<nil>)
FAIL
FAIL monkey/evaluator 0.008s
不过,我们知道如何让它通过。 我们需要为 *ast.HashLiterals 使用另一个 case 分支扩展我们的 Eval 函数。
// evaluator/evaluator.go
func Eval(node ast.Node, env *object.Environment) object.Object {
// [...]
case *ast.HashLiteral:
return evalHashLiteral(node, env)
// [...]
}
这里的 evalHashLiteral 函数可能看起来很吓人,但相信我,它不会咬人:
// evaluator/evaluator.go
func evalHashLiteral(
node *ast.HashLiteral,
env *object.Environment,
) object.Object {
pairs := make(map[object.HashKey]object.HashPair)
for keyNode, valueNode := range node.Pairs {
key := Eval(keyNode, env)
if isError(key) {
return key
}
hashKey, ok := key.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", key.Type())
}
value := Eval(valueNode, env)
if isError(value) {
return value
}
hashed := hashKey.HashKey()
pairs[hashed] = object.HashPair{Key: key, Value: value}
}
return &object.Hash{Pairs: pairs}
}
当迭代 node.Pairs 时, keyNode 是第一个被评估的。 除了检查对 Eval 的调用是否产生错误之外,我们还对评估结果进行类型断言:它需要实现 object.Hashable 接口,否则它不能用作哈希键。 这正是我们添加 Hashable 定义的原因。
然后我们再次调用 Eval 来评估 valueNode。 如果对 Eval 的调用也没有产生错误,我们可以将新产生的键值对添加到我们的对映射中。 我们通过调用 HashKey() 为恰当命名的 hashKey 对象生成一个 HashKey 来做到这一点。 然后我们初始化一个新的 HashPair,指向键和值并将其添加到对中。
这就是全部。 测试现在正在通过:
$ go test ./evaluator
ok monkey/evaluator 0.007s
这意味着我们已经可以开始在 REPL 中使用哈希文字了:
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> {"name": "Monkey", "age": 0, "type": "Language", "status": "awesome"}
{age: 0, type: Language, status: awesome, name: Monkey}
棒极了! 但是我们还不能从哈希中取出元素,这有点降低了它们的用处:
>> let bob = {"name": "Bob", "age": 99};
>> bob["name"]
ERROR: index operator not supported: HASH
这就是我们现在要解决的问题。
还记得我们在评估器中添加到 evalIndexExpression 的 switch 语句吗? 你还记得我告诉你我们要添加另一个案例分支吗? 好吧,我们来了!
但首先我们需要添加一个测试函数,以确保通过索引表达式访问哈希中的值有效:
// evaluator/evaluator_test.go
func TestHashIndexExpressions(t *testing.T) {
tests := []struct {
input string
expected interface{}
}{
{
`{"foo": 5}["foo"]`,
5,
},
{
`{"foo": 5}["bar"]`,
nil,
},
{
`let key = "foo"; {"foo": 5}[key]`,
5,
},
{
`{}["foo"]`,
nil,
},
{
`{5: 5}[5]`,
5,
},
{
`{true: 5}[true]`,
5,
},
{
`{false: 5}[false]`,
5,
},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
integer, ok := tt.expected.(int)
if ok {
testIntegerObject(t, evaluated, int64(integer))
} else {
testNullObject(t, evaluated)
}
}
}
就像在 TestArrayIndexExpressions 中一样,我们确保使用索引运算符表达式产生正确的值 - 只是这次使用哈希。 当从哈希中检索值时,这里的不同测试用例使用字符串、整数或布尔哈希键。 所以,本质上,测试真正断言的是各种数据类型实现的HashKey方法被正确调用。
并且为了确保使用对象作为未实现 object.Hashable 的哈希键会产生错误,我们可以向我们的 TestErrorHandling 测试函数添加另一个测试:\
// evaluator/evaluator_test.go
func TestErrorHandling(t *testing.T) {
tests := []struct {
input string
expectedMessage string
}{
// [...]
{
`{"name": "Monkey"}[fn(x) { x }];`,
"unusable as hash key: FUNCTION",
},
}
// [...]
}
运行go test
现在结果如期失败:
$ go test ./evaluator
--- FAIL: TestErrorHandling (0.00s)
evaluator_test.go:228: no error object returned. got=*object.Null(&{})
--- FAIL: TestHashIndexExpressions (0.00s)
evaluator_test.go:611: object is not Integer. got=*object.Null (&{})
evaluator_test.go:611: object is not Integer. got=*object.Null (&{})
evaluator_test.go:611: object is not Integer. got=*object.Null (&{})
evaluator_test.go:611: object is not Integer. got=*object.Null (&{})
evaluator_test.go:611: object is not Integer. got=*object.Null (&{})
FAIL
FAIL monkey/evaluator 0.007s
这意味着我们已经准备好在 evalIndexEx pression 的 switch 语句中添加另一个 case 分支:
// 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)
case left.Type() == object.HASH_OBJ:
return evalHashIndexExpression(left, index)
default:
return newError("index operator not supported: %s", left.Type())
}
}
新的 case 分支调用了一个新函数:evalHashIndexExpression。 我们已经知道 evalHashIndexExpression 必须如何工作,因为我们之前成功测试了 object.Hashable 接口的使用 - 在我们的测试中和评估哈希文字时。 所以这里没有惊喜:
// evaluator/evaluator.go
func evalHashIndexExpression(hash, index object.Object) object.Object {
hashObject := hash.(*object.Hash)
key, ok := index.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", index.Type())
}
pair, ok := hashObject.Pairs[key.HashKey()]
if !ok {
return NULL
}
return pair.Value
}
将 evalHashIndexExpression 添加到 switch 语句使测试通过:
$ go test ./evaluator
ok monkey/evaluator 0.007s
我们现在可以成功地从我们的哈希中检索值! 不相信我? 认为测试在骗我们? 我伪造了测试输出? 不能吗? 整本书都是li..什么? 不,看这个
$ go run main.go
Hello mrnugget! This is the Monkey programming language!
Feel free to type in commands
>> let people = [{"name": "Alice", "age": 24}, {"name": "Anna", "age": 28}];
>> people[0]["name"];
Alice
>> people[1]["age"];
28
>> people[1]["age"] + people[0]["age"];
52
>> let getName = fn(person) { person["name"]; };
>> getName(people[0]);
Alice
>> getName(people[1]);
Anna
< 4.4数组 | > 4.6总决赛 |
---|