Raku 程式設計/惰性列表和饋送
在大多數傳統的計算系統中,資料物件被分配到一個固定的尺寸,並且它們的數值被填充到記憶體中的空間。例如,在C中,如果我們宣告一個數組int a[10],那麼陣列a將是一個固定大小,有足夠的空間來精確地儲存 10 個整數。如果我們想要儲存 100 個整數,我們需要分配一個 100 個的空間。如果我們想要儲存一百萬個,我們需要分配一個那樣的尺寸的陣列。
讓我們考慮一個問題,我們想要計算一個乘法表,一個二維陣列,其中陣列中給定單元格的值是它兩個索引的乘積。以下是一個簡單的迴圈,可以用它來生成具有高達 N 個因子的表
int products[N][N];
int i, j;
for(i = 0; i < N; i++) {
for(j = 0; j < N; j++) {
products[i][j] = i * j;
}
}
建立這個表可能需要一段時間來執行所有N2個操作。當然,一旦我們初始化了表,在其中查詢一個值的速度就非常快。這裡要考慮的另一個問題是,我們最終計算的值比我們實際使用的多,所以這是浪費的努力。
現在,讓我們看看一個執行相同操作的函式
int product(int i, int j) {
return i * j;
}
這個函式不需要任何啟動時間來初始化它的值,但它確實需要每次呼叫時額外的計算時間來計算結果。它的啟動速度比陣列快,但在每次訪問時比陣列花費的時間更多。
結合這兩個想法,我們得到了惰性列表。
惰性列表就像陣列,但有一些主要的差異
- 它們不一定要用預定義的大小來宣告。它們可以是任何大小,甚至無限長。
- 它們只在需要時計算它們的數值,並且只在需要時計算所需的值。
- 一旦它們的數值被計算出來,它們就可以被儲存起來以供快速查詢。
與惰性列表相反的是積極列表。積極列表立即計算並存儲所有它們的值,就像 C 中的陣列一樣。積極列表不能是無限長的,因為它們需要在記憶體中儲存它們的值,而計算機沒有無限的記憶體。
Raku 同時擁有兩種型別的列表,並且它們在內部處理,無需程式設計師干預。可以是惰性的列表將被惰性地處理。不能是惰性的列表將被積極地計算和儲存。惰性列表在儲存空間和計算開銷方面提供了優勢,因此 Raku 嘗試預設使用它們。Raku 還提供了一些結構,可以用來支援惰性和提高列表計算的效能。
我們已經看到了範圍。範圍預設情況下是惰性的,這意味著範圍中的所有值不一定會在你將它們分配給陣列時被計算出來
my @lazylist = 1..20000; # Doesn't calculate all 20,000 values
由於它們的惰性,範圍甚至可以是無限的
my @lazylist = 1..Inf; # Infinite values!
迭代器是特殊的資料庫項,它們一次遍歷一個複雜的資料物件中的一個元素。想想文字編輯器程式中的游標;游標讀取一次按鍵,將字元插入到它當前的位置,然後移動到下一個位置等待下一個按鍵。透過這種方式,可以一次插入一個很長的字元陣列,而你,編輯器,不必手動移動游標。
以同樣的方式,Raku 中的迭代器自動遍歷陣列和散列表,自動跟蹤你在陣列中的當前位置,這樣你就不必自己去跟蹤了。我們之前討論迴圈時已經看到了迭代器的一個用法,儘管我們沒有用“迭代器”這個名字來稱呼它們。以下兩個迴圈執行相同的函式
my @x = 1, 2, 3, 4, 5;
loop(my int $i = 0; $i < @x.elems; $i++) {
@x[$i].say;
}
for @x { # Same, but much shorter!
$_.say;
}
第一個迴圈使用$i變數手動遍歷@x陣列,跟蹤當前位置,並使用$i < @x.length測試來確保我們還沒有到達末尾。在第二個迴圈中,for關鍵字為我們建立了一個迭代器。迭代器自動跟蹤我們在陣列中的當前位置,自動檢測我們何時到達陣列的末尾,並自動將每個後續的值載入到$_預設變數中。值得一提的是,我們可以使用一些 Raku 的慣用法來使它更短
.say for @x;
迭代器是任何實現了Iterator角色的物件。我們稍後會談到角色,但現在說一個角色是一個標準介面,其他類可以參與其中就足夠了。因為它們可以是任何類,只要它有一個標準介面,迭代器就可以做我們定義它們做的事情。迭代器可以輕鬆地遍歷陣列和散列表,但專門定義的型別也可以遍歷樹、圖、堆、檔案和所有其他資料結構和概念。
如果一個數據庫項有一個關聯的迭代器型別,它可以透過.Iterator()方法訪問。此方法在大多數情況下被諸如for迴圈之類的結構在內部呼叫,但如果你真的需要,你可以訪問它。
饋送提供了一種很好的圖形化方式來顯示資料在複雜賦值語句中的移動位置。饋送有兩個端點,一個“鈍”端和一個“尖”端。鈍端連線到一個數據源,它是一個值列表。尖端連線到一個接收器,接收器可以一次接收至少一個元素。饋送可以用來從右到左或從左到右傳送資料,這取決於饋送指向的方向。
my @x <== 1..5;
say @x # 1, 2, 3, 4, 5
@x ==> @y ==> print # 1, 2, 3, 4, 5
say @y # 1, 2, 3, 4, 5
分層饋送將資料從一個饋送移動到另一個饋送。但是,有兩個點的饋送將追加到饋送鏈中的最後一個專案
my @x = 1..5;
@x ==> map {$_ * 2} ==> @y;
say @x; # 1, 2, 3, 4, 5
say @y; # 2, 4, 6, 8, 10
@x ==>>
@y ==> @z;
say @z # 1, 2, 3, 4, 5, 2, 4, 6, 8, 10
我們可以使用gather和take關鍵字編寫我們自己的迭代器型別。這兩個關鍵字的行為與我們之前見過的尖塊非常相似。但是,與尖塊不同的是,gather/take 可以返回值。與尖塊一樣,gather/take 可以與迴圈結合起來形成自定義迭代器。
gather用來定義一個特殊的塊。該塊的程式碼可以執行任意計算,並使用take返回一個值。以下是一個例子
my $x = gather {
take 5;
}
say $x; # 5
這本身沒什麼用。但是,我們現在可以將它與迴圈結合起來返回一個很長的值列表
my @x = gather for 1..5 {
take $_ * 2;
}
say @x # 2, 4, 6, 8, 10
take運算子執行兩個動作:它獲取它傳遞的值的捕獲,並將其作為gather塊結果之一返回,並且它返回它被傳遞以供儲存的值。我們可以很容易地將這種行為與state變數結合起來,以遞迴地使用值。
my @x = gather for 1..5 {
state $a = $_;
$a = take $_ + $a;
}
say @x; # 2, 4, 7, 11, 16