實作基本的陣列操作。
在函式式語言中,length、map和reduce這類陣列操作非常常見。請在不使用現有函式的情況下,實作一系列基本的陣列操作。
要實作的操作,其確切數量和名稱會依 track 而有所不同,以避免與現有名稱衝突,但你會實作的一般操作包括:
append(給定兩個陣列,把第二個陣列中的所有項目加到第一個陣列的尾端);concatenate(給定一連串的陣列,把所有陣列中的所有項目合併成一個攤平的陣列);filter(給定一個述詞和一個陣列,回傳所有 predicate(item) 為 True 的項目所組成的陣列);length(給定一個陣列,回傳其中項目的總數);map(給定一個函式和一個陣列,回傳對所有項目套用 function(item) 所得結果的陣列);foldl(給定一個函式、一個陣列和初始累加器,從左側將每個項目摺疊(縮減)進累加器);foldr(給定一個函式、一個陣列和一個初始累加器,從右側將每個項目摺疊(縮減)進累加器);reverse(給定一個陣列,回傳一個包含所有原始項目、但順序相反的陣列)。請注意,傳遞給摺疊函式(foldl、foldr)的引數順序很重要。
這個練習會需要用到 Odin 的參數多型(更常被稱為泛型)。如果你沒看過這個功能,這裡有一份快速概要帶你入門。
參數多型是一種程式語言的特性,讓開發者不必把程式碼裡用到的型別寫得那麼明確(更泛用,泛型這個名稱就是這樣來的),同時又能維持型別安全。當然,這對 Odin 這類強型別語言才有意義。
我們先從一個例子開始吧。假設你想把陣列裡的所有元素都加上一個固定值,這算是個很簡單的問題。
incr_array_int :: proc(a: []int, by: int) -> []int {
new_array := make([]int, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
那如果現在,浮點數也需要同樣的功能呢?
incr_array_f64 :: proc(a: []f64, by: f64) -> []f64 {
new_array := make([]f64, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
接著換成無號整數、32 位元浮點數,還有其他更多型別呢?
沒多久你就會累積出一大堆程序,做著完全相同的事,只是處理的型別不同。萬一你需要更新邏輯,就得確保每一種變體都改到,這會變成很沉重的維護負擔。另一個惱人之處是,你得幫每個程序取不同的名字,因為 Odin 不支援隱式的程序多載(你還是可以用顯式多載,不過那是另一個練習的故事了)。
Odin 是一門務實的語言,它透過參數多型提供了這個問題的解法。只要編譯器能在編譯期推斷出參數的型別,你就可以給它一個泛用的名字,例如T。我們來改寫上面的程序吧:
incr_array :: proc(a: []$T, by: T) -> []T {
new_array := make([]T, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
注意我們把所有型別標註(int 或 f64)都換成了T,而T第一次出現時前面帶著一個錢字號($T)。$T這種型別寫法告訴 Odin 編譯器,名稱T是這個型別的泛用名稱,會在編譯期間被替換成實際的名稱。而且,既然編譯器現在已經知道泛型型別T,之後出現的同一型別只要標上你所選的型別名稱(T)就行了。
你現在就可以寫出像這樣的程式碼:
a_int := incr_array([]int{1, 2, 3}, 10)
a_f64 := incr_array([]f64{1.0, 2.0, 3.0}, 10.0)
在第一道敘述裡,Odin 編譯器會把第一個參數的型別([]int)對應到泛型參數的型別([]$T),推斷出T = int,然後編譯出一份把後續所有T都換成int的版本(等同於上面那個特化版本incr_array_int())。如果你在第一個參數的定義中省略了錢字號,編譯器就會在目前的套件以及匯入清單裡尋找名為T的型別,而且很可能回報一個編譯錯誤Error: Undeclared name: T。
第二道敘述的運作方式和第一道完全相同,只是編譯器辨識出T是f64。
泛型型別習慣上會用單一字母來命名(T和E很常用)
現在你對參數多型,也就是泛型型別,應該已經有足夠的認識,可以挑戰 List Operations 練習了。