Skip to content

Latest commit

 

History

History
123 lines (93 loc) · 7.74 KB

File metadata and controls

123 lines (93 loc) · 7.74 KB

3.4表示对象

等一下,什么?你从来没说过Monkey是面向对象的!是的,我从来没说但它不是。 为什么我们需要“对象系统”呢? 那么称之为“价值系统”或“对象表示”。关键是,我们需要定义我们的“eval”函数返回什么。 我们需要一个系统来表示我们的 AST 所代表的值或我们在内存中评估 AST 时生成的值。

让我们来评估下面的Monkey代码:

let a = 5;
//[...]
a + a;

如您所见,我们将整数文字 5 绑定到名称 a。 然后事情就发生了。 没关系。 重要的是,当我们稍后遇到 a + a 表达式时,我们需要访问 a 绑定到的值。 为了计算 a + a,我们需要得到 5。在 AST 中,它表示为 *ast.IntegerLiteral,但是我们如何在计算 AST 的其余部分时跟踪和表示 5?

在解释性语言中构建值的内部表示时,有很多不同的选择。关于这个话题,有很多智慧在世界解释器和编译器的代码库中传播。每个解释器都有关于自己的方式来表示值,总是与之前的解决方案略有不同,根据需求进行了调整解释型语言。

有些使用宿主语言的本机类型(整数、布尔值等)来表示解释语言中的值,而不是用任何东西包装。 在其他语言中,值/对象仅表示为指针,而在某些编程语言中,本机类型和指针是混合的。

为什么种类这么多?一方面,宿主语言不同。你如何表示解释语言的字符串取决于如何在解释器的语言中表示字符串用Ruby编写的解释器不能像用C编写的解释器那样表示值。

并且不仅仅是宿主语言不同,而是语言解释器的实现不同。一些解释器语言可能只需要原始数据类型的表示,如整形、字符或字节。但在其他情况下,您将拥有列表、字典、函数或复合数据类型。这些差异导致对价值表示的要求截然不同。

除了宿主语言和解释语言,对设计影响最大的值表示的实现是由此产生的执行速度和内存在评估程序时消耗。如果你想构建一个快速的解释器,你无法摆脱缓慢而臃肿的对象系统。如果你要自己写出垃圾收集器,您需要考虑它如何跟踪系统中的值。但是,另一方面,如果你不关心性能,那么保持简单是有意义的并且与易于理解,直到出现进一步的要求。

关键是:有很多不同的方式来表示宿主语言中解释语言的值。 了解这些不同表示的最好(也可能是唯一)方法是实际通读一些流行解释器的源代码。 我衷心推荐 Wren 源代码,其中包括两种类型的值表示,通过使用编译器标志启用/禁用。

除了宿主语言中值的表示之外,还有一个问题是如何将这些值及其表示暴露给解释语言的用户。 这些值的“公共 API”是什么样的?

例如,Java 向用户提供“原始数据类型”(int、byte、short、long、float、double、boolean、char)和引用类型。 原始数据类型在 Java 实现中没有大量表示,它们紧密映射到它们的本地对应物。 另一方面,引用类型是对宿主语言中定义的复合数据结构的引用。

在 Ruby 中,用户无权访问“原始数据类型”,没有像原生值类型那样存在,因为一切都是对象,因此包装在内部表示中。 Ruby 在内部不区分字节和类 Pizza 的实例:两者都是相同的值类型,包装不同的值。

有无数种方法可以向编程语言的用户公开数据。 选择哪一个取决于语言设计,同样取决于性能要求。 如果你不关心性能,一切都会好起来的。 但是如果你这样做了,你需要做出一些明智的决定来实现你的目标。

我们对象系统的基础

由于我们仍然关心我们的 Monkey 解释器的性能,我们选择了简单的方法:我们将在评估 Monkey 源代码时遇到的每个值表示为对象,我们设计的接口。 每个值都将被包装在一个结构体中,该结构体实现了这个 Object 接口。

在一个新object包我们定义了Object接口和ObjectType类型:

package object

type ObjectType string

type Object interface {
    Type() ObjectType
    Inspect() string
}

这非常简单,看起来很想我们在带有Token和TokenType类型的token包中所作的。除了像Token这样的结构体之外,Object类型是一个接口。原因是每个值都需要不同的内部表示,并且定义两种不同的结构体类型比尝试将布尔值和整数放入同一个结构体字段更容易。

这时候我们只需要三个数据类型在Monkey解释器中:null,booleans和integers。让我们开始用实现integer表示和构建我们的对象系统。

integer

object.Integer类型和你预期的一样简短:

// object/object.go

import (
    "fmt"   
)

type Integer struct {
    Value int64
}

func (i *Integer) Inspect() string { return fmt.Sprintf("%d", i.Value) }

无论何时我们输入一个integer字段在源代码中我们首先返回它到一个ast.IntegerLiteral并且然后,当评估这个AST节点时,我们返回一个object.Integer,将值保存在我们的结构中并传递对该结构的引用。

为了让object.Integer实现object.Object接口,它仍然需要一个Type方法返回它的ObjectType。就像我们对token.TokenType所作的一样,我们为每个ObjectType定义常量:

// object/object.go
import "fmt"

type ObjectType string

const (
    INTEGER_OBJ = "INTEGER"
)

正如我所说的,这几乎就是我们在token包所作的。有了它我们就可以将Type()方法添加到*object.Integer:

// object/object.go
func (i *Integer) Type() ObjectType { return INTEGER_OBJ }

我们完成了Integer! 转换成另一种数据类型:布尔值。

布尔值

如果你期待本节的大事,我很抱歉让你失望。 object.Boolean 非常小:

// object/object.go

const (
// [...]
    BOOLEAN_OBJ = "BOOLEAN"
)
type Boolean struct {
    Value bool
}
func (b *Boolean) Type() ObjectType { return BOOLEAN_OBJ }
func (b *Boolean) Inspect() string { return fmt.Sprintf("%t", b.Value) }

只是一个包装单个值的结构体,一个布尔值。

我们已经接近完成对象系统的基础,现在我们需要做的最后的事是在我们开始我们的 Eval 函数之前,是表示一个不存在的值。

Null

Tony Hoare 在 1965 年引入了对 ALGOL W 语言的空引用,并称这是他的“十亿美元的错误”。 自从引入以来,无数系统因引用“null”而崩溃,该值表示没有值。 至少可以说,Null(或某些语言中的“nil”)并没有最好的声誉。

我和自己争论 Monkey 是否应该为 null。 一方面,是的,如果该语言不允许 null 或 null 引用,则使用起来会更安全。 但另一方面,我们并不是要重新发明轮子,而是要学习一些东西。 而且我发现在我可以使用 null 时,只要有机会使用它,我就会三思而后行。 有点像在你的车里放一些爆炸性的东西会让你开得更慢、更小心。 它真的让我很欣赏编程语言设计中的选择。 这是我认为值得的。 因此,让我们实现 Null 类型,并在以后使用它时保持仔细观察和稳定。

// object/object.go
const (
// [...]
    NULL_OBJ = "NULL"
)
type Null struct{}

func (n *Null) Type() ObjectType { return NULL_OBJ }
func (n *Null) Inspect() string { return "null" }

object.Null 和 object.Boolean 和 object.Integer 一样是一个结构体,只是它不包装任何值。 它代表没有任何值。

添加 object.Null 后,我们的对象系统现在能够表示布尔值、整数和空值。 这足以开始使用 Eval。

< 3.3一个Tree-Walking解释器 > 3.5评估表达式