學習軌道
/
Julia
Julia
/
練習
/
起司俱樂部
起司俱樂部

起司俱樂部

學習練習

簡介

嚴格來說,Higher Order Function就是至少符合以下其中一項的函式:

  • 接受一個函式作為它的其中一個引數。
  • 回傳一個函式作為結果。

在函式式程式設計的世界裡,這個詞的用法通常比較狹窄。 它通常指的是filter、map和reduce這類函式,它們會把傳入的函式套用到集合的每個元素上。

操作集合

課程大綱進行到這裡,我們已經看過各種把某個操作套用到可疊代集合(例如 Vector)中所有元素的作法:

  • 使用迴圈(就像數位運算問世以來大多數程式語言的做法)。
  • 使用推導式(Python 風格)。
  • 使用廣播(這是 Julia 特有的語法,雖然大量借鑑了 R、Matlab 和 NumPy)。

這個概念會聚焦在高階函式(任何函式式語言,例如 Haskell 或 F#,都有這種東西)。

其他可能的方法還包括:

  • 遞迴(就像 ML 系語言那樣)。
    • Julia 允許這麼做,但少了尾呼叫最佳化,就會有堆疊溢位的風險。
  • 使用巨集的超程式設計(傳統上是 Lisp 的特色)。
    • 這在進階的 Julia 程式設計中很常用,但大多數情況下請_謹慎使用_。
    • 其他做法通常寫起來更容易,除錯也輕鬆得多。

篩選

filter()函式會接收一個回傳布林值的函式,並把它套用到某個集合上。 只有回傳true的元素會包含在回傳值裡,而回傳值的_基本_型態和輸入相同(見下文)。

julia> filter(iseven, 1:6)
3-element Vector{Int64}:
 2
 4
 6

# String is a collection of Chars, so String in -> String out
julia> filter(!isascii, "Hrōðgār")
"ōðā"

# tuple input -> tuple output
julia> filter(iseven, (1, 2, 3, 4, 5))
(2, 4)

面對多維陣列時,filter會把輸入的維度攤平,回傳一個 Vector:這是輸出型態與輸入型態相符這個規則最主要的例外。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> filter(isodd, m)
3-element Vector{Int64}:
 1
 5
 3

上面的例子用的是內建函式,但在這種情境下,匿名函式的使用也非常普遍。

julia> filter(x -> x % 3 == 0, 1:20)
6-element Vector{Int64}:
  3
  6
  9
 12
 15
 18

另外也有原地修改的版本filter!(),就像這個概念中的許多函式一樣。

映射

map()函式會把函式套用到每個元素,藉此轉換整個集合。 在簡單的情況下,這和廣播很像,輸出的形狀會和輸入一致。

julia> map(√, [1, 4, 9])
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> map(x -> x^2 + 1, 1:4)
4-element Vector{Int64}:
  2
  5
 10
 17

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> map(√, m)
2×3 Matrix{Float64}:
 1.0  1.41421  1.73205
 2.0  2.23607  2.44949

map()也能對多個集合逐元素操作。

julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
 3
 8

概念上,我們可以把它想成等同於先對多個輸入集合執行zip(),再對中繼結果的每個元素執行map()。 這只是粗略的類比,並不代表實作上就是這樣!

和zip()一樣,形狀不相符的集合會被截斷到最小的那個維度。

有時候我們只需要傳入函式的副作用,例如寫入資料庫,或對陣列執行push!。 這時候就可以用高階函式foreach(),它一律回傳nothing。

歸約

reduce()函式會接收一個有兩個引數的函式,並把它套用到集合上,藉此降低維度。

抽象聽起來可能有點難懂,但想想sum()或prod()這類函式,它們接收一個集合,回傳單一值。

julia> sum(1:4) # add
10

julia> prod(1:4) # multiply
24

這些特殊函式經過高度最佳化,只要能用就該用。 其他例子還包括maximum()和minimum()、邏輯函式all()和any(),以及許多統計函式。

純粹為了示範,看看用更通用的reduce()實作同樣功能會是什麼樣子(回想一下,中綴運算子+和*在內部其實就是函式)。

julia> reduce(+, 1:4) # add
10

julia> reduce(*, 1:4) # multiply
24

和sum()及其他聚合函式一樣,reduce()可以接受一個選用的關鍵字引數dims,用來指定要歸約的維度。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> reduce(+, m; dims=1)
1×3 Matrix{Int64}:
 5  7  9

這些都是簡單的例子,因為加法和乘法既滿足交換律(1+2 == 2+1),也滿足結合律((1+2)+3 == 1+(2+3))。

但這絕不是通例! 就連減法和除法這麼常見的運算,也不滿足結合律。

還有一個額外的問題:在大型集合上,浮點誤差會累積,所以從左到右歸約的結果,可能和從右到左的結果略有不同。

Julia 的reduce函式往哪個方向進行,取決於實作,並沒有保證。

如果想明確控制方向,可以用foldl()和foldr()這兩個函式,顧名思義分別從「左」和「右」開始(實際上對 Vector 來說是從最上面和最下面)。

julia> foldl(-, 1:3) # (1 - 2) - 3
-4

julia> foldr(-, 1:3) # 1 - (2 - 3)
2

請注意,這兩個函式是給可以視為一維的集合用的,會回傳純量結果。 foldl和foldr不支援使用dims引數,只有reduce支援。

MapReduce

在各種程式設計領域裡,把map操作和reduce結合起來都很常見。

我們可以依序先執行map,再對中繼集合執行reduce。 但這麼做充其量只是效率不彰,而且集合一大,擴展性就會非常糟糕。

因此_強烈_建議改用合併後的mapreduce()函式。 它可以實作效能好上許多的演算法,把 map 和 reduce 的操作交錯進行。

第一個引數是要用來 map 的函式,第二個引數是 reduce 運算子。

julia> mapreduce(x -> x^2 + 1, +, 1:3)
17

# equivalent to (2 + 5 + 10)
julia> sum(map(x -> x^2 + 1, 1:3))
17

一如預期,Julia 也有mapfoldl()和mapfoldr()函式,用在方向很重要的情況。

說明

我們正在籌辦一個起司俱樂部,它會運用機器學習,根據顧客的歷史紀錄與口味,挑選出新的起司來提供給熱愛起司的顧客。

新會員必須填寫一份初步問卷,讓我們能先蒐集一些基本資料。 從中我們發現,有一部分的顧客評價方式特別極端,批評時缺乏細膩的區分。 由於這種情況可能會無可挽回地讓更細膩的演算法產生偏差,因此我們另外設置了一套演算法來處理他們的需求。 你需要提供一些輔助函式來整理他們的資料。

Note

雖然解決下列任務的方式可能不只一種,但每一項都能用一個不同的單一高階函式來解決。

1. 將顧客分類

評分系統以五顆星為基準,也就是由整數1:5組成。 極端顧客只會給出1或5的評分,而我們想知道某位顧客是否展現出這種行為。

實作all_15(),它會接收一個評分向量,當所有評分都是1或5時回傳true,否則回傳 false。

julia> ratings = [2, 3, 4, 4, 1];

julia> all_15(ratings)
false

julia> ratings = [1, 5, 5, 1, 5];

julia> all_15(ratings)
true

2. 分出極端顧客

我們需要把評價較極端的顧客和其他顧客分開。

實作emphatics(),它會接收一個包含顧客與評分的字典。 回傳一個類似的字典,只保留評分僅有1或5星的顧客。

julia> ratings = ([2, 3, 5, 1, 1], [1, 1, 5, 5, 1], [4, 5, 5, 3, 2], [5, 5, 1, 1, 5]);

julia> names = ("c1", "c2", "c3", "c4");

julia> customers = Dict(zip(names, ratings))
Dict{String, Vector{Int64}} with 4 entries:
  "c2" => [1, 1, 5, 5, 1]
  "c1" => [2, 3, 5, 1, 1]
  "c3" => [4, 5, 5, 3, 2]
  "c4" => [5, 5, 1, 1, 5]

julia> emphatics(customers)
Dict{String, Vector{Int64}} with 2 entries:
  "c2" => [1, 1, 5, 5, 1]
  "c4" => [5, 5, 1, 1, 5]

3. 將評分轉為二進位

由於極端顧客只使用1和5的評分,把這些評分改成0和1在運算上會更方便。

實作tobinary(),它會接收一個極端評分的向量。 回傳二進位評分,其中1已改成0,5已改成1。

julia> ratings = [1, 1, 5, 5, 1];

julia> tobinary(ratings)
5-element Vector{Int64}:
 0
 0
 1
 1
 0

4. 將評分變成矩陣

我們的演算法使用Matrix作為輸入,所以我們需要把資料轉換成矩陣。

實作tobinarymatrix(),它會接收一個由極端評分向量組成的向量。 回傳轉換後資料的Matrix,其中每個評分向量都是矩陣中的一列。

julia> customersratings = [[1, 1, 5, 5, 1],[5, 5, 1, 1, 5]];

julia> tobinarymatrix(customersratings)
2×5 Matrix{Int64}:
 0  0  1  1  0
 1  1  0  0  1
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Julia Exercism

準備好開始 起司俱樂部 了嗎?

註冊 Exercism,透過 35 個概念128 個練習 和真人引導來學習並精通 Julia,全部免費。