跳轉到內容

鸚鵡虛擬機器/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($/) );
}
華夏公益教科書