iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0
Software Development

Functional Programming in Kotlin系列 第 19

Curried function

前面看過兩次,今天終於要來介紹咖哩(誤)了,坦白說,在我的日常開發中並不會使用到這個概念,所以我沒辦法說服自己說,這是一個很方便的東西,以下到內容是我查閱資料整理出來的心得,如果內容有錯或是不適合還請大家提出更正。

Simple sample

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 ,就會叫出這樣的視窗:

https://user-images.githubusercontent.com/7949400/94370449-a4c7b780-0122-11eb-98a1-d235f0fbb211.png

選擇第二項就可以自動幫你生成下圖這樣的介面(還是要手動修改回傳值,預設會出現 (P2) -> (P2) -> R ,要改成 (P1) -> (P2) -> R ):

https://user-images.githubusercontent.com/7949400/94370457-aa250200-0122-11eb-91ca-be644cf5f7a0.png

接下來的任務就非常簡單拉,要實作的內容很少,其實就跟上面的實作長得幾乎一模一樣:

fun <P1, P2, R> ((P1, P2) -> R).curried(): (P1) -> (P2) -> R {
    return { p1: P1 ->
        { p2: P2 ->
            this(p1, p2)
        }
    }
}

Usecase

在 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 ,再注入需要的共同參數,就可以分享到各種不同的地方去使用。

Lifting

有了 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


上一篇
Function type - Another Algebraic Data Type
下一篇
Functional Data Structure
系列文
Functional Programming in Kotlin30

1 則留言

0
taiansu
iT邦新手 5 級 ‧ 2020-10-07 12:25:31

如果只單獨提到 curry 這個行為,那的確是沒什麼好用的。要用上這個,大多會是一個連續技。

首先要意識到有某個多參數的函式,裡面有某個參數拿到的時間點特別晚。或說這個函式有很多人調用它,但是取得其中某個參數的手法不同。那麼就會這麼做:

  1. 先用 curry 把這個函式變成惰性求值的函式
  2. 用 partial application 將已知的參數填進去
  3. 用函式組合組出不同樣貌,但只差一個參數的函式。在很多語言裡有 decorator 這個概念,其實它就是個函式組合。

廣告一下在我寫的這篇的中間裡有個範例:
https://ithelp.ithome.com.tw/articles/10247503

我要留言

立即登入留言