鸚鵡虛擬機器/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($/) );
}