跳转到内容

鹦鹉虚拟机/Squaak 教程/哈希表和数组

来自维基教科书,开放的书籍,开放的世界

欢迎来到第八集!这是本教程的倒数第二集。在本集之后,我们将完成 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($/) );
}
华夏公益教科书