前面看過兩次,今天終於要來介紹咖哩(誤)了,坦白說,在我的日常開發中並不會使用到這個概念,所以我沒辦法說服自己說,這是一個很方便的東西,以下到內容是我查閱資料整理出來的心得,如果內容有錯或是不適合還請大家提出更正。
class Post(val userID: String, val content: String)
上面這是一個簡單的類別,需要兩個 String 來組合而成,如果我要用一個 function 來代表建構它的函式,就會是像以下這樣:
val postCreator: (String, String)-> Post = { userID, content -> Post(userID, content)}
所謂的柯里化(curried)就是將參數一個一個拿出來,放到回傳值裡組成另外一個 function:
// (String) -> (String) -> Post 跟 (String) -> ( (String) -> Post ) 是一樣的
val curriedPostCreator: (String) -> (String) -> Post = { userId ->
{ content ->
Post(userId, content)
}
}
上面這是兩個參數的情況,三個參數、四個參數的話,就可以依此類推在返回值的 Type 上繼續往前加。而這個 curriedPostCreator
的實作雖然乍看之下有點恐怖,但是他其實本質上是雙層的 lamda,相信雙層迴圈或是雙層類別對大家來說都不陌生,在這裡就是替換成 lambda,習慣了就不會覺得可怕了。 接下來,上面的實作,可以再近一步的進行 generalization。
val postCreator: (String, String)-> Post = { userID, content -> Post(userID, content)}
val curriedPostCreator: (String) -> (String) -> Post = postCreator.curried()
為了完成 generalization ,新增一個叫 curried()
的 extension function, 這個 function 可以自動幫我們完成柯里化。有一種方法可以非常快速的實作出來這個 function ,如果你的手邊有 Intellij IDE ,將上面的的程式碼打上去後,接下來同時按下 option + Enter
,就會叫出這樣的視窗:
選擇第二項就可以自動幫你生成下圖這樣的介面(還是要手動修改回傳值,預設會出現 (P2) -> (P2) -> R
,要改成 (P1) -> (P2) -> R
):
接下來的任務就非常簡單拉,要實作的內容很少,其實就跟上面的實作長得幾乎一模一樣:
fun <P1, P2, R> ((P1, P2) -> R).curried(): (P1) -> (P2) -> R {
return { p1: P1 ->
{ p2: P2 ->
this(p1, p2)
}
}
}
在 Android 的開發中,我們會使用到各種畫面上配置的操作,例如顏色、圖片(Drawable)、尺寸(Dimens)等等。如果要獲取到這些資源,需要用到一個 Android 上下文的類別 (Context) ,與資源 Id, 如下方所示:
val colorWhite = ContextCompat.getColor(context, R.color.white)
val colorPrimary = ContextCompat.getColor(context, R.color.colorPrimary)
val colorPrimaryDark = ContextCompat.getColor(context, R.color.colorPrimaryDark)
但是每次都要有一個 context 總覺得有點囉唆啊...一般來說,如果我們想要縮短程式碼的話會這樣做:
// Non-functional way
fun getColor(id: Int): Int {
return ContextCompat.getColor(this, id)
}
val colorWhite = getColor(R.color.white)
但是這有 Side effect 不是嗎?既然我們都要學 Functional Programming 了,就試著用更 functional 的方式來解決問題吧!首先, ContextCompat.getColor
是一個需要兩個參數的函式,所以我們可以對他做柯里化:
val curriedColorGetter: (Context) -> (Int) -> Int = ContextCompat::getColor.curried()
接下來,把 context 注入進去
val simpleColorGetter: (Int) -> Int = curriedColorGetter(context)
最後就可以使用這個 simpleColorGetter
來減少樣板程式碼(boilerplate code)
private fun sample(context: Context) {
val curriedColorGetter: (Context) -> (Int) -> Int = ContextCompat::getColor.curried()
val simpleColorGetter: (Int) -> Int = curriedColorGetter(context)
val colorWhite = simpleColorGetter(R.color.white)
val colorPrimary = simpleColorGetter(R.color.colorPrimary)
val colorPrimaryDark = simpleColorGetter(R.color.colorPrimaryDark)
}
柯里化所帶來的好處是可以讓我們寫少一點的樣板程式碼,當我們需要共同參數的時候,可以先對他做 curried
,再注入需要的共同參數,就可以分享到各種不同的地方去使用。
有了 Curried 的概念後,再回來提一下之前介紹過的 Functor ,幫大家複習一下,Functor 是一個 Category 跟 Category 之間的轉換,Type A
轉成 Type F[A]
,Type B
轉成 Type F[B]
,那 function A → B
呢?就是 F[A → B]
,同時也是 F[A] → F[B]
,而這個 function 在 Functor 中的轉換就稱為 lifting :
lifting: ((A) -> B) -> ((F[A]) -> F[B])
由於 function 本身具有結合律,所以這些括號我可以全部拿掉:
(A) -> (B) -> (F[A]) -> F[B]
再重新組合一下變為:
((A) -> (B)) -> F[A] -> F[B]
這些 Type 的順序沒變,只是把括弧換位置了,這個調整過後的樣子就形成了三個區塊,也是最上面介紹的柯里化之後所長的樣子,所以我們來試試看對他做“反柯里化”。
(A -> B, F[A]) -> F[B]
反柯里化之後形成了兩個參數的 function type。然後,對一個 function 來說,參數的順序不會影響到結果,只要把實作相對應的地方也換過來就好。
(F[A], A -> B) -> F[B]
再把這 lambda 寫法轉為我們都熟悉的 kotlin function 寫法,並且把這個函式稱為 fmap
:
fun <A, B> fmap(fa: F<A>, transform: (A) -> B): F<B>
最後,如果這個 F 是一個泛型的類別的話, fmap
就可以寫在這類別裡面而不需要帶入 F ,因為類別本身就是第一個參數
class F<A> {
fun <B> fmap(transform: (A) -> B): F<B> = { ...}
}
登登!熟悉的 map
出現了! map
這個 function 就是在做 Functor 在做的事沒錯!至於 F 呢,可以替換成 List, Maybe, Either, Observable ...
就如同今天所說的, curried 可以做的不只是只有兩個參數的函式,三個參數、四個參數也可以轉換,請讀者試試看自己實作這樣的 extension function 吧!
fun <P1, P2, P3, R> ((P1, P2, P3) -> R).curried(): (P1) -> (P2) -> (P3) -> R {
TODO()
}
fun <P1, P2, P3, P4, R> ((P1, P2, P3, P4) -> R).curried(): (P1) -> (P2) -> (P3) -> (P4) -> R {
TODO()
}
Reference:
http://hughfdjackson.com/javascript/why-curry-helps/
https://jigsawye.gitbooks.io/mostly-adequate-guide/content/ch4.html
如果只單獨提到 curry 這個行為,那的確是沒什麼好用的。要用上這個,大多會是一個連續技。
首先要意識到有某個多參數的函式,裡面有某個參數拿到的時間點特別晚。或說這個函式有很多人調用它,但是取得其中某個參數的手法不同。那麼就會這麼做:
廣告一下在我寫的這篇的中間裡有個範例:
https://ithelp.ithome.com.tw/articles/10247503