鹦鹉虚拟机/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($/) ); }