學習軌道
/
Odin
Odin
/
練習
/
陣列操作
陣列操作

陣列操作

中等

說明

實作基本的陣列操作。

在函式式語言中,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 練習了。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Odin Exercism

準備好開始 陣列操作 了嗎?

註冊 Exercism,透過 73 個練習 和真人引導來學習並精通 Odin,全部免費。

深入探索 陣列操作!

享受一場實用的遞迴入門,探索陣列操作的指令式與函式風格替代做法,並深入鑽研尾端遞迴與累加器函式。