Kotlin 版圖解 Functor、Applicative 與 Monad
本文是從 Haskell 版 Functors, Applicatives, And Monads In Pictures 翻譯而來的 Kotlin 版。 我同時翻譯了中英文兩個版本,英文版在這裡。
與從 Swift 版翻譯而來的 Kotlin 版不同的是,本文是直接從 Haskell 版原文翻譯而來的。
這是一個簡單的值:
我們也知道如何將一個函式應用到這個值上:
這很簡單。 那麼擴充套件一下,我們說任何值都可以放到一個上下文中。 現在你可以把上下文想象為一個可以在其中裝進值的盒子:
現在,將一個函式應用到這個值上時,會根據上下文的不同而得到不同的結果。
這就是 Functor、 Applicative、 Monad、 Arrow 等概念的基礎。
Maybe
資料型別定義了兩種相關上下文:
sealed class Maybe<out T> {
object `Nothing#` : Maybe<Nothing>() {
override fun toString(): String = "Nothing#"
}
data class Just<out T>(val value: T) : Maybe<T>()
}
很快我們就會看到將函式應用到 Just<T>
上 還是應用到 Nothing#
上會有多麼不同。
首先我們來說說 Functor 吧!
注: 這裡用
Nothing#
取代原文的Nothing
,因為在 Kotlin 中Nothing
是一個特殊型別,參見 Nothing 型別。 另外 Kotlin 有自己的表達可選值的方式,並非使用Maybe
型別這種方式,參見空安全。
Functor
當一個值被包裝在上下文中時,你無法將一個普通函式應用給它:
這就輪到 fmap
出場了。
fmap
翩翩而來,從容應對上下文。
fmap
知道如何將函式應用到包裝在上下文中的值上。
例如,你想將 {it + 3}
應用到 Just(2)
上。
使用 fmap
如下:
> Maybe.Just(2).fmap { it + 3 }
Just(value=5)
嘭!
fmap
向我們展示了它的成果。
但是 fmap
怎麼知道如何應用該函式的呢?
究竟什麼是 Functor 呢?
在 Haskell 中 Functor
是一個型別類。
其定義如下:
在 Kotlin 中,可以認為 Functor
是一種定義了 fmap
方法/擴充套件函式的型別。
以下是 fmap
的工作原理:
所以我們可以這麼做:
> Maybe.Just(2).fmap { it + 3 }
Just(value=5)
而 fmap
神奇地應用了這個函式,因為 Maybe
是一個 Functor。
它指定了 fmap
如何應用到 Just
上與 Nothing#
上:
fun <T, R> Maybe<T>.fmap(transform: (T) -> R): Maybe<R> = when(this) {
Maybe.`Nothing#` -> Maybe.`Nothing#`
is Maybe.Just -> Maybe.Just(transform(this.value))
}
當我們寫 Maybe.Just(2).fmap { it + 3 }
時,這是幕後發生的事情:
那麼然後,就像這樣,fmap
,請將 it + 3
應用到 Nothing#
上如何?
> Maybe.`Nothing#`.fmap { x: Int -> x + 3 }
Nothing#
注: 這裡該 lambda 表示式的引數必須顯式標註型別,因為 Kotlin 中有很多型別可以與整數(
Int
)相加。
就像《黑客帝國》中的 Morpheus,fmap
知道都要做什麼;如果你從 Nothing#
開始,那麼你會以 Nothing#
結束!
fmap
是禪道。
現在它告訴我們了 Maybe
資料型別存在的意義。
例如,這是在一個沒有 Maybe
的語言中處理一個資料庫記錄的方式:
post = Post.find_by_id(1)
if post
return post.title
else
return nil
end
而在 Kotlin 中:
findPost(1).fmap(::getPostTitle)
如果 findPost
返回一篇文章,我們就會通過 getPostTitle
獲取其標題。
如果它返回 Nothing#
,我們就也返回 Nothing#
!
非常簡潔,不是嗎?
我們還可以為 fmap
定義一箇中綴操作符 ($)
(在 Haskell 中是 <$>
),並且這樣更常見:
infix fun <T, R> ((T) -> R).`($)`(maybe: Maybe<T>) = maybe.fmap(this)
::getPostTitle `($)` findPost(1)
再看一個示例:如果將一個函式應用到一個 Iterable
(Haksell 中是 List
)上會發生什麼?
Iterable
也是 functor!
我們可以為其定義 fmap
如下:
fun <T, R> Iterable<T>.fmap(transform: (T) -> R): List<R> = this.map(transform)
好了,好了,最後一個示例:如果將一個函式應用到另一個函式上會發生什麼?
{x: Int - > x + 1}.fmap {x: Int -> x + 3}
這是一個函式:
這是一個應用到另一個函式上的函式:
其結果是又一個函式!
> fun <T, U, R> ((T) -> U).fmap(transform: (U) -> R) = { t: T -> transform(this(t)) }
> val foo = {x: Int -> x + 2}.fmap {x: Int -> x + 3}
> foo(10)
15
所以函式也是 functor! 對一個函式使用 fmap,其實就是函式組合!
Applicative
Applicative 又提升了一個層次。 對於 Applicative,我們的值像 Functor 一樣包裝在一個上下文中:
但是我們的函式也包裝在一個上下文中!
嗯。
我們繼續深入。
Applicative 並沒有開玩笑。
Applicative 定義了 (*)
(在 Haskell 中是 <*>
),它知道如何將一個 包裝在上下文中的 函式應用到一個 包裝在上下文中的 值上:
即:
infix fun <T, R> Maybe<(T) -> R>.`(*)`(maybe: Maybe<T>): Maybe<R> = when(this) {
Maybe.`Nothing#` -> Maybe.`Nothing#`
is Maybe.Just -> this.value `($)` maybe
}
Maybe.Just {x: Int -> x + 3} `(*)` Maybe.Just(2) == Maybe.Just(5)
使用 (*)
可能會帶來很多有趣的情況。
例如:
infix fun <T, R> Iterable<(T) -> R>.`(*)`(iterable: Iterable<T>) =
this.flatMap { iterable.map(it) }
有了這個定義,我們可以將一個函式列表應用到一個值列表上:
> listOf<(Int) -> Int>({it * 2}, {it + 3}) `(*)` listOf(1, 2, 3)
[2, 4, 6, 4, 5, 6]
這裡有 Applicative 能做到而 Functor 不能做到的事情。 如何將一個接受兩個引數的函式應用到兩個已包裝的值上?
> {y: Int -> {x: Int -> x + y}} `($)` Maybe.Just(5)
Just(value=(kotlin.Int) -> kotlin.Int) // 等於 `Maybe.Just {x: Int -> x + 5}`
> Maybe.Just {x: Int -> x + 5} `($)` Maybe.Just(4)
錯誤 ??? 這究竟是什麼意思,這個函式為什麼包裝在 JUST 中?
Applicative:
> {y: Int -> {x: Int -> x + y}} `($)` Maybe.Just(5)
Just(value=(kotlin.Int) -> kotlin.Int) // 等於 `Maybe.Just {x: Int -> x + 5}`
> Maybe.Just {x: Int -> x + 5} `(*)` Maybe.Just(3)
Just(value=8)
Applicative
把 Functor
推到一邊。
“大人物可以使用具有任意數量引數的函式,”它說。
“裝備了 ($)
與 (*)
之後,我可以接受具有任意個數未包裝值引數的任意函式。
然後我傳給它所有已包裝的值,而我會得到一個已包裝的值出來!
啊啊啊啊啊!”
> {y: Int -> {x: Int -> x + y}} `($)` Maybe.Just(5) `(*)` Maybe.Just(3)
Just(value=15)
我們也可以定義另一個 Applicative 的函式 liftA2
:
fun <T> ((x: T, y: T) -> T).liftA2(m1: Maybe<T>, m2: Maybe<T>) =
{y: T -> {x: T -> this(x, y)}} `($)` m1 `(*)` m2
並使用 liftA2
做同樣事情:
> {x: Int, y: Int -> x * y}.liftA2(Maybe.Just(5), Maybe.Just(3))
Just(value=15)
Monad
如何學習 Monad 呢: 1. 取得電腦科學博士學位。 2. 然後把它扔掉,因為在本節中你並不需要!
Monad 增加了一個新的轉變。
Functor 將一個函式應用到一個已包裝的值上:
Applicative 將一個已包裝的函式應用到一個已包裝的值上:
Monad 將一個返回已包裝值的函式應用到一個已包裝的值上。
Monad 有一個函式 ))=
(在 Haskell 中是 >>=
,讀作“繫結”)來做這個。
讓我們來看個示例。
老搭檔 Maybe
是一個 monad:
假設 half
是一個只適用於偶數的函式:
fun half(x: Int) = if (x % 2 == 0)
Maybe.Just(x / 2)
else
Maybe.`Nothing#`
如果我們餵給它一個已包裝的值呢?
我們需要使用 ))=
來將我們已包裝的值塞進該函式。
這是 ))=
的照片:
以下是它的工作方式:
> Maybe.Just(3) `))=` ::half
Nothing#
> Maybe.Just(4) `))=` ::half
Just(value=2)
> Maybe.`Nothing#` `))=` ::half
Nothing#
內部發生了什麼?
Monad
是 Haskell 中的另一個型別類。
這是它(在 Haskell 中)的定義的片段:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
其中 >>=
是:
在 Kotlin 中,可以認為 Monad
是一種定義了這樣中綴函式的型別:
infix fun <T, R> Monad<T>.`))=`(f: ((T) -> Monad<R>)): Monad<R>
所以 Maybe
是一個 Monad:
infix fun <T, R> Maybe<T>.`))=`(f: ((T) -> Maybe<R>)): Maybe<R> = when(this) {
Maybe.`Nothing#` -> Maybe.`Nothing#`
is Maybe.Just -> f(this.value)
}
這是與 Just(3)
互動的情況!
如果傳入一個 Nothing#
就更簡單了:
你還可以將這些呼叫串聯起來:
> Maybe.Just(20) `))=` ::half `))=` ::half `))=` ::half
Nothing#
注: Kotlin 內建的空安全語法可以提供類似 monad 的操作,包括鏈式呼叫:
fun Int?.half() = this?.let { if (this % 2 == 0) this / 2 else null } val n: Int? = 20 n?.half()?.half()?.half()
太酷了!
於是現在我們知道 Maybe
既是 Functor
、又是 Applicative
還是 Monad
。
現在我們來看看另一個例子:IO
monad:
注: 由於 Kotlin 並不區分純函式與非純函式,因此根本不需要 IO monad。 這只是一個模擬:
data class IO<out T>(val `(-`: T) infix fun <T, R> IO<T>.`))=`(f: ((T) -> IO<R>)): IO<R> = f(this.`(-`)
具體來看三個函式。
getLine
沒有引數並會獲取使用者輸入:
fun getLine(): IO<String> = IO(readLine() ?: "")
readFile
接受一個字串(檔名)並返回該檔案的內容:
typealias FilePath = String
fun readFile(filename: FilePath): IO<String> = IO(File(filename).readText())
putStrLn
接受一個字串並輸出之:
fun putStrLn(str: String): IO<Unit> = IO(println(str))
所有這三個函式都接受普通值(或無值)並返回一個已包裝的值。
我們可以使用 ))=
將它們串聯起來!
getLine() `))=` ::readFile `))=` ::putStrLn
太棒了!
前排佔座來看 monad 展示!
Haskell 還為我們提供了名為 do
表示法的語法糖:
foo = do
filename <- getLine
contents <- readFile filename
putStrLn contents
它可以在 Kotlin 中模擬(其中 Haskell 的 <-
操作符被替換為 (-
屬性與賦值操作)如下:
fun <T> `do` (ioOperations: () -> IO<T>) = ioOperations()
val foo = `do` {
val filename = getLine().`(-`
val contents = readFile(filename).`(-`
putStrLn(contents)
}
結論
- (Haskell 中的)functor 是實現了
Functor
型別類的資料型別。 - (Haskell 中的)applicative 是實現了
Applicative
型別類的資料型別。 - (Haskell 中的)monad 是實現了
Monad
型別類的資料型別。 Maybe
實現了這三者,所以它是 functor、 applicative、 以及 monad。
這三者有什麼區別呢?
- functor: 可通過
fmap
或者($)
將一個函式應用到一個已包裝的值上。 - applicative: 可通過
(*)
或者liftA
將一個已包裝的函式應用到已包裝的值上。 - monad: 可通過
))=
或者liftM
將一個返回已包裝值的函式應用到已包裝的值上。
所以,親愛的朋友(我覺得我們現在是朋友了),我想我們都同意 monad 是一個簡單且高明的主意(譯註:原文是 SMART IDEA(tm))。 現在你已經通過這篇指南潤溼了你的口哨,為什麼不拉上 Mel Gibson 並抓住整個瓶子呢。 請參閱《Haskell 趣學指南》的《來看看幾種 Monad》。 其中包含很多我已經炫耀過的東西,因為 Miran 深入這些方面做的非常棒。
譯註: Miran 即 Miran Lipovača 是《Haskell 趣學指南》英文原版 Learn You a Haskell 的作者。
在此向 Functors, Applicatives, And Monads In Pictures 原作者 Aditya Bhargava 致謝, 向 Learn You a Haskell 作者 Miran Lipovača 以及 MnO2、Fleurer 等《Haskell 趣學指南》中文版譯者致謝。
本文也發在我的個人部落格上:https://hltj.me/kotlin/2017/08/25/kotlin-functor-applicative-monad-cn.html。
相關文章
- Typescript版圖解Functor , Applicative 和 MonadTypeScript圖解APP
- 函數語言程式設計 - Swift中的Functor(函子)、Monad(單子)、Applicative函數程式設計SwiftAPP
- 圖解 Monad圖解
- What is functor in Haskell ?Haskell
- C++ lambda 表示式與「函式物件」(functor)C++函式物件
- 易用版Popupwindow by Kotlin瞭解一下Kotlin
- promise is a monad?Promise
- Haskell學習-functorHaskell
- 仿函式——Functor函式
- Category_theory and FunctorGo
- 資料結構與演算法-圖解版資料結構演算法圖解
- JavaScript的MonadJavaScript
- Haskell學習-monadHaskell
- Promise是Monad嗎?Promise
- Kotlin 1.4的Dokaa Alpha版Kotlin
- [譯]Functor 與 Category (軟體編寫)(第六部分)Go
- Kotlin的互操作——Kotlin與Java互相呼叫KotlinJava
- Android版 kotlin協程入門(二):kotlin協程的關鍵知識點初步講解AndroidKotlin
- 完整解釋 Monad -- 程式設計師範疇論入門程式設計師
- Kotlin——集合詳解Kotlin
- Kotlin 1.3.60正式版釋出Kotlin
- Kotlin 1.3.50正式版釋出Kotlin
- Kotlin 1.3正式版釋出Kotlin
- Kotlin 背景圓頭像圖Kotlin
- Kotlin——中級篇(二): 屬性與欄位詳解Kotlin
- 16.Kotlin星投影與泛型約束詳解Kotlin泛型
- CPS 與 Kotlin coroutineKotlin
- Kotlin 與 Java 對比KotlinJava
- 《Haskell趣學指南》筆記之 Applicative 函子Haskell筆記APP
- 讀《圖解TCP/IP(第5版)》圖解TCP
- 《圖解 C# 教程 第 5 版》與效能優化(附 Unity 專案)圖解C#優化Unity
- Kotlin 變數詳解:宣告、賦值與最佳實踐指南Kotlin變數賦值
- Kotlin 迴圈與函式詳解:高效程式設計指南Kotlin函式程式設計
- Kotlin註解之JvmNameKotlinJVM
- Monad和Monoid的定義Mono
- 談談我對Monad的理解
- Java的Monad和懶賦值Java賦值
- Kotlin---集合與遍歷Kotlin