鹦鹉虚拟机/Squaak 教程/哈希表和数组
第一集: 介绍
第二集: 窥探编译器内部
第三集: Squaak 细节和第一步
第四集: PAST 节点和更多语句
第五集: 变量声明和作用域
第六集: 作用域和子程序
第七集: 运算符和优先级
第八集: 哈希表和数组
第九集: 总结和结论
欢迎来到第八集!这是本教程的倒数第二集。在本集之后,我们将完成 Squaak 语言的完整实现。本集重点介绍聚合数据结构:数组和哈希表。我们将讨论分配给它们和构造它们的语法。我们将看到实现动作方法非常容易,几乎是微不足道的。之后,我们将对聚合作为参数进行一些说明,以及它们在作为子程序参数传递时与基本数据类型有何不同。
除了整数、浮点数和字符串等基本数据类型之外,Squaak 还具有两种聚合数据类型:数组和哈希表。数组是一个可以存储值序列的对象。此序列中的值可以是不同类型,这与某些语言不同,这些语言要求数组的所有元素都具有相同的类型。以下显示了使用数组的示例
grades[0] = "A" grades[1] = "A+" grades[2] = "B+" grades[3] = "C+"
哈希表存储键值对;键用作索引来存储值。键必须是字符串常量,但值可以是任何类型。以下显示了一个示例
lastnames{"larry"} = "wall"
lastnames{"allison"} = "randal"
就像可以使用整数文字 (42) 和字符串文字 ("hello world") 为变量赋值一样,您也可以使用数组文字。以下是对此的语法规则
rule array_constructor {
'[' [ <expression> [',' <expression>]*]? ']'
{*}
}
下面显示了一些示例
foo = [] bar = [1, "hi", 3.14] baz = [1, [2, 3, 4] ]
第一个示例创建一个空数组并将其分配给 foo。第二个示例显示了三个元素的构造,将数组分配给 bar。请注意,一个数组的元素可以是不同类型。第三个示例显示了嵌套数组的构造。这意味着元素 baz[1][0] 评估为值 2(索引从 0 开始)。
除了数组文字之外,Squaak 还支持哈希表文字,可以通过哈希表构造函数来构造。以下表达了此语法
rule hash_constructor {
'{' [<named_field> [',' <named_field>]* ]? '}'
{*}
}
rule named_field {
<string_constant> '=>' <expression>
{*}
}
下面显示了一些示例
foo = {}
bar = { "larry" => "wall", "allison" => "randal" }
baz = { "a" => { "b" => 42} }
第一行创建一个空哈希表并将其分配给 foo。第二行创建一个具有两个字段的哈希表:"larry" 和 "allison"。它们各自的值是:"wall" 和 "randal"。第三行显示哈希表也可以嵌套。在那里,构造了一个哈希表,它有一个名为 "a" 的字段,它的值是另一个哈希表,包含一个名为 "b" 的字段,它的值为 42。
您可能认为实现对数组和哈希表的支持看起来相当困难。嗯,事实并非如此。实际上,实现相当简单。首先,我们将更新 primary 的语法规则
rule primary {
<identifier> <postfix_expression>*
{*}
}
rule postfix_expression {
| <index> {*} #= index
| <key> {*} #= key
}
rule index {
'[' <expression> ']'
{*}
}
rule key {
'{' <expression> '}'
{*}
}
一个 primary 对象现在是一个标识符,后面跟着任意数量的后缀表达式。后缀表达式要么是一个哈希表键,要么是一个数组索引。允许任意数量的后缀表达式可以将数组和哈希表相互嵌套,从而允许我们编写例如
foo{"key"}[42][0]{"hi"}
当然,作为 Squaak 程序员,您必须确保 foo 实际上是一个哈希表,并且 foo{"key"} 生成一个数组,等等。实现这一点实际上非常简单。首先,让我们看看如何实现动作方法 index。
method index($/) {
my $index := $( $<expression> );
my $past := PAST::Var.new( $index,
:scope('keyed'),
:viviself('Undef'),
:vivibase('ResizablePMCArray'),
:node($/) );
make $past;
}
首先,我们检索表达式的 PAST 节点。然后,我们通过创建一个 PAST::Var 节点并将其作用域设置为 'keyed' 来创建一个键控变量访问操作。如果 PAST::Var 节点具有键控作用域,则第一个子节点将被评估为聚合对象,第二个子节点将被评估为该聚合上的索引。
但是等等!我们刚刚创建的 PAST::Var 节点只有一个子节点!
这就是更新后的 primary 动作方法的用武之地。如下所示。
method primary($/) {
my $past := $( $<identifier> );
for $<postfix_expression> {
my $expr := $( $_ );
$expr.unshift( $past );
$past := $expr;
}
make $past;
}
首先,检索标识符的 PAST 节点。然后,对于每个后缀表达式,我们获取 PAST 节点,并将 (当前) $past 放入其中。实际上,(当前) $past 被设置为 $expr 的第一个子节点。而且您知道 $expr 包含什么:那是键控变量访问节点,它是在动作方法 index 中创建的。
之后,$past 被设置为 $expr;要么存在另一个后缀表达式,在这种情况下,这个 $past 将被设置为该下一个后缀表达式的第一个子节点,要么当前的 $past 被设置为结果对象。
要实现数组和哈希表构造函数,我们将利用 Parrot 的调用约定 (PCC)。PCC 支持可选参数、命名参数和贪婪参数。如果您是荷兰人,您可能认为贪婪参数会发出很多噪音(“slurpen”是荷兰语动词,意思是小心地喝,您通常会在饮料很热的时候这样做,从而在过程中发出噪音),但您错了。贪婪参数将存储尚未存储在其他参数中的所有剩余参数(这意味着只能有一个贪婪(位置)参数,并且它应该放在所有正常(位置)参数之后)。Parrot 将自动创建一个聚合来存储这些剩余的参数。除了位置贪婪参数之外,您还可以定义一个命名贪婪参数,它将存储所有剩余的命名参数,在存储了所有正常(命名)参数之后。
您现在可能感到困惑。
让我们看一个例子,因为这个问题值得存储一些脑细胞。
.sub foo
.param pmc a
.param pmc b
.param pmc c :slurpy
.param pmc k :named('x')
.param pmc l :named('y')
.param pmc m :named :slurpy
.end
foo(1, 2, 3, 4, 6 :named('y'), 5 :named('x'), 7 :named('p'), 8 :named('q') )
这将导致以下映射
a: 1
b: 2
c: {3, 4}
k: 5
l: 6
m: {"p"=>7, "q"=>8}
因此,在位置参数 (a、b) 之后,c 被声明为贪婪参数,存储所有剩余的位置参数。参数 k 和 l 被声明为命名参数,它们的名称分别是 "x" 和 "y"。使用这些名称,可以传递值。在命名参数之后,是参数 m,它被标记为命名和贪婪。此参数将存储所有剩余的命名参数,这些参数尚未由正常的命名参数存储。
对我们来说,有趣的参数是 "c" 和 "m"。对于位置贪婪参数,Parrot 创建一个数组,而对于命名贪婪参数,则创建一个哈希表。这恰好是我们需要的!实现数组和哈希构造函数变得微不足道
.sub '!array'
.param pmc fields :slurpy
.return (fields)
.end
.sub '!hash'
.param pmc fields :named :slurpy
.return (fields)
.end
然后,数组和哈希表构造函数可以编译为对相应 Parrot 子程序的子程序调用,并将所有字段作为参数传递。(请注意,这些名称以 "!" 开头,这不是有效的 Squaak 标识符。这可以防止我们在正常的 Squaak 代码中调用这些子程序)。
所有数据类型,无论是基本数据类型还是聚合数据类型,都用 Parrot Magic Cookies (PMC) 表示。PMC 是 Parrot 可以处理的四种内置数据类型之一;其他三种是整数、浮点数和字符串。目前,PCT 只能生成代码来处理 PMC,而不能处理其他基本数据类型。Parrot 为其四种内置数据类型中的每一种都有寄存器。整数、浮点数和字符串寄存器存储实际数据值,而 PMC 寄存器存储 PMC 对象的引用。这会影响将 PMC 作为参数传递时的处理方式。当将 PMC 作为参数传递时,调用的子例程将获得对 PMC 引用的访问权限;换句话说,PMC 是通过引用传递的。这意味着子例程可以更改调用方传递的原始参数。当然,这取决于正在生成的指令,以及调用的子例程对引用执行的操作。在 Squaak 中,当传递基本数据值时,这些值不能被调用的子例程更改。当将新值赋给参数时,将创建一个全新的对象并将其绑定到参数标识符。原始参数不会发生任何变化。然而,聚合数据类型则以不同的方式处理。当调用的子例程将值赋给参数的索引或哈希表字段时,原始参数将受到影响。换句话说,基本数据类型具有按值语义,而聚合数据类型具有按引用语义。以下是一个简短的示例来演示这一点。
sub foo(a,b,c)
a = 42
b[0] = 1
c{"hi"} = 2
end
var a = 0
var b = []
var c = {}
foo(a,b,c)
print(a, b[0], c{"hi"} ) # prints 0, 1, 2
接下来做什么?
[edit | edit source]这是讨论实现细节以使 Parrot(运行)Squaak 的最后一集。完成本集的练习后,您的实现应该相当完整。下一集将是本系列的最后一集,我们将回顾我们所做的工作,并使用一个不错的演示程序来演示我们的语言。
练习
[edit | edit source]- 问题 1
我们已经展示了如何通过实现 index 的动作方法来实现数组的键控变量访问。同样的原理可以应用于哈希表的键控访问。实现 key 的动作方法。
method key($/) {
my $key := $( $<expression> );
make PAST::Var.new( $key, :scope('keyed'),
:vivibase('Hash'),
:viviself('Undef'),
:node($/) );
}
- 问题 2
实现 array_constructor 和 hash_constructor 的动作方法。使用 PAST::Op 节点,并将 pasttype 设置为 'call'。使用 "name" 属性来指定要调用的子例程的名称(例如::name("!array"))。注意,所有哈希字段都必须作为命名参数传递。查看 PDD26 以了解如何执行此操作,并查找 "named " 方法。
method named_field($/) {
my $past := $( $ );
my $name := $( $ );
## the passed expression is in fact a named argument,
## use the named() accessor to set that name.
$past.named($name);
make $past;
}
method array_constructor($/) {
## use the parrot calling conventions to
## create an array,
## using the "anonymous" sub !array
## (which is not a valid Squaak name)
my $past := PAST::Op.new( :name('!array'),
:pasttype('call'),
:node($/) );
for $<expression> {
$past.push($($_));
}
make $past;
}
method hash_constructor($/) {
## use the parrot calling conventions to
## create a hash, using the "anonymous" sub
## !hash (which is not a valid Squaak name)
my $past := PAST::Op.new( :name('!hash'),
:pasttype('call'),
:node($/) );
for $<named_field> {
$past.push($($_));
}
make $past;
}
- 问题 3
我们想为访问哈希表键添加一些语法糖。与其编写 foo{"key"},我想编写 foo.key。当然,这仅适用于不包含空格等的键。添加相应的语法规则(称为 "member"),它允许使用这种语法,并编写相关的动作方法。确保此成员名称被转换为字符串。
- 提示:使用 PAST::Val 节点进行字符串转换。
rule postfix_expression {
| <key> {*} #= key
| <member> {*} #= member
| <index> {*} #= index
}
rule member {
'.' <identifier>
{*}
}
method member($/) {
my $member := $( $<identifier> );
## x.y is syntactic sugar for x{"y"},
## so stringify the identifier:
my $key := PAST::Val.new( :returns('String'),
:value($member.name()),
:node($/) );
## the rest of this method is the same
## as method key() above.
make PAST::Var.new( $key, :scope('keyed'),
:vivibase('Hash'),
:viviself('Undef'),
:node($/) );
}