軌道
/
Julia
Julia
/
課程大綱
/
函式合成
函式

函式合成 在 Julia

1 個練習

關於 函式合成

Julia 鼓勵程式設計師盡可能把程式碼放進可被 JIT 編譯的函式裡,而設計上,建立許多小型函式本來就有很好的效能。

這樣往往會留下許多小型又簡單的函式,而要完成較複雜的工作時,就得把它們組合起來。

一個顯而易見的做法是巢狀呼叫函式。下面這個例子非常刻意,但足以說明重點。

julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

這種做法的缺點是,巢狀越深,可讀性下降得越快。

我們需要更簡單、更有彈性的做法。

合成

這是數學家鍾愛的技巧,而 Julia 沿用了數學的語法。

任意數量的函式都可以被composed在一起,靠的就是∘運算子(輸入\circ再按 Tab)。結果可以當成單一個函式來使用。

julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse

julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])

有幾點值得注意:

  • 起始的函式排列順序與巢狀時相同,執行時則是由右至左。
  • 合成時不容易使用廣播,但可以在呼叫合成後的函式時套用。

管線化

另一種做法可以想成是_程式設計師_的做法,而不是_數學家_的做法。

Pipelines長久以來一直用在 Unix shell 指令碼裡,近年來也在主流程式語言中流行起來(F# 有時被認為是率先採用它們的先驅)。

基本概念是先從一些資料開始,再讓它依序流經一連串函式,藉此得到結果。

管線運子是|>(F# 與近年版本的 R 也是如此),不過 Julia 也有廣播版本.|>。

julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

現在的執行順序嚴格地由左至右,每個函式的輸出會順著箭頭的方向流動,成為下一個函式的輸入。

限制、變通做法與其他選項

用來示範合成與管線化的函式,全都只接受_單一_引數,這絕非巧合。

有些純函式語言會把_第一個_引數送進函式,但允許一併帶入其他引數。

相較之下,Julia 在管線中只接受函式_名稱_(或同等的東西),不能附帶任何額外的引數。

這背後有重要的技術原因(和currying並非該語言設計的標準組成有關)。_許多_對 currying 毫無概念的人,只要接受這件事就好:這個限制並非粗心的疏忽,未來版本的 Julia 也不太可能改變它。

變通做法

我們需要的,是能完成各種工作的單引數函式。幸好,在 Julia 裡定義新函式很簡單。

最簡單的方式,就是用一個anonymous function。舉例來說,假設我們有一個輸入字串,想依底線來切分:

julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

接著就能像之前一樣,把這個向量送進其他函式。

在這個情況下,用括號把匿名函式括起來是可有可無的,但一般來說,這是減少歧義的好方法。

同樣地,我們也可以在程式較早的地方建立具名函式,再視需要重複使用。

Closures超出本概念的範圍,但任何從其他語言熟悉它們的人都能看出,它們提供了更有彈性的方式來建立單引數函式。

julia> function makesplit(sep)
           fs(str) = split(str, sep)
           fs
       end
makesplit (generic function with 1 method)

julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)

julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

一旦定義好makesplit(),就能用它來處理任何分隔符號。

請注意,makesplit('_')是_函式呼叫_,它會求出另一個函式,而這個函式接著會從管線接收輸入。

如果這看起來令人困惑,一開始這是正常的(但隨著練習會越來越清楚)。

其他選項

關於讓 Julia 內建的管線更有彈性,一直有很長的討論,但各種建議彼此不相容,始終沒有達成共識。

與此同時,使用者採取了慣常的做法,建立各種可安裝的套件來滿足特定需求。這些在 Exercism 上都無法運作,但如果你有興趣,不妨看看:

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

學習 函式合成