本文是从 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 呢:
- 取得计算机科学博士学位。
- 然后把它扔掉,因为在本节中你并不需要!
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 趣学指南》中文版译者致谢。
相关推荐
除了上述功能,`kategory`还包含许多其他实用工具,如Monoid(表示可结合的运算)、Monad(用于组合计算)以及Applicative Functor(在纯上下文中执行计算)。这些概念源自函数式编程语言,但在`kategory`中被很好地...
《疯狂Android讲义》(Kotlin版) 是一本深度探讨Android开发的专业书籍,专注于使用现代编程语言Kotlin进行Android应用开发。源码17章涵盖了该书中的一个重要章节,可能是关于高级Kotlin特性、Android框架集成或是...
它包括最流行的数据类型,类型类和抽象,例如Option,Try,Either,IO,Functor,Applicative,Monad等,还有许多使用户能够定义的能力。rrow是Kotlin中用于Typed Functional Programming的库。 Arrow旨在在Kotlin库...
“实战”部分包括Kotlin与Java互操作、使用Kotlin集成Spring Boot开发WEB服务端、使用Kotlin集成Gradle开发、使用Kotlin和Anko的Android开发、使用Kotlin DSL、Kotlin文件IO操作与多线程、使用Kotlin Native。...
1. **更好的互操作性**:Kotlin一直以其与Java的无缝互操作性著称,1.3.21版本进一步增强了这一点,使得与Java库和代码的交互更加流畅。 2. **性能提升**:编译速度的优化使得开发过程更快,同时生成的字节码也更...
“实战”部分包括Kotlin与Java互操作、使用Kotlin集成Spring Boot开发WEB服务端、使用Kotlin集成Gradle开发、使用Kotlin和Anko的Android开发、使用Kotlin DSL、Kotlin文件IO操作与多线程、使用Kotlin Native。...
它提供了一组强大的类型类(Typeclass)和范畴论(Category Theory)概念的实现,如Functor、Applicative、Monad等,这些抽象概念可以帮助开发者编写出更易于理解和维护的代码。 2. **数据结构与类型** Arrow库...
6. **Android开发**:作为Android程序员的必备,书中将详细介绍如何在Android项目中使用Kotlin,包括Kotlin与Java的互操作、Android组件(如Activity、Fragment)的Kotlin实现,以及如何利用Kotlin的特性优化Android...
《Kotlin插件1.3.72在Android Studio 4.0中的应用与详解》 Kotlin,作为Google官方推荐的Android开发语言,已经逐渐成为开发者们的首选。本文将详细解析Kotlin插件1.3.72版本在Android Studio 4.0中的使用方法及其...
Kotlin数据类型与变量 Kotlin流程控制:循环与分支 Kotlin函数详解 Kotlin类与对象 Kotlin继承与多态 Kotlin接口与抽象类 Kotlin异常处理 Kotlin标准库介绍 Kotlin协程基础 Kotlin并发编程 Kotlin与Android开发 ...
1. **更好的互操作性**:Kotlin 1.6.21改进了与Java和其他语言的互操作性,特别是在处理Java反射和注解时更加灵活。 2. **模块化支持**:新版本提供了模块化编译功能,这有助于大型项目管理和优化构建速度。 3. **...
Kotlin的目标是扩展Java的功能,同时保持与Java的互操作性,它集成了现代编程范式,包括面向对象、函数式编程等。 在基础语法方面,Kotlin的包名定义在源文件的开头,不必和文件夹路径一致,因此源文件可以放在任意...
【Kotlin项目——图片查看器】是一个以Kotlin编程语言实现的应用程序,旨在提供一个用户友好的界面,用于浏览和管理个人图片集。这个项目主要关注于Kotlin在Android开发中的应用,展示了一些核心的Kotlin特性以及...
Kotlin与Java的互操作性是其另一大优势。这意味着你可以直接在Kotlin项目中调用Java代码,反之亦然,这对于已有大量Java代码的项目来说是个福音。同时,Kotlin的兼容性使其能在Java虚拟机(JVM)上运行,也可以编译...
《Kotlin中文版参考手册》是一本全面介绍Kotlin编程语言的权威指南,旨在帮助中文开发者深入理解和掌握Kotlin的各项特性和用法。Kotlin,由JetBrains公司开发,是一种现代、静态类型的编程语言,主要面向Java虚拟机...
《Kotlin中文版教程》是一本详尽介绍Kotlin编程语言的资源,旨在帮助中文学习者深入理解并熟练掌握Kotlin。Kotlin是由JetBrains公司开发的一种现代、静态类型的编程语言,广泛应用于Android应用开发,同时也适用于...
书中会详细解析Kotlin与Java在Android开发中的差异,以及如何迁移现有的Java项目至Kotlin。此外,还将讨论Anko库的使用,这是一个简化Android开发的库,提供了更简洁的DSL(领域特定语言)来替代传统的XML布局。 书...
**Kotlin手册中文版** Kotlin是一种现代、静态类型的编程语言,主要面向Java虚拟机(JVM)和Android平台,同时也支持JavaScript和原生(Native)编译。它由JetBrains公司开发,旨在提高开发效率,减少代码量,并...
在Kotlin与Java的互操作性方面,文档阐述了如何在Kotlin中调用Java代码,反之亦然,使得现有Java项目能平滑地过渡到Kotlin。 对于JavaScript开发者,Kotlin/JS提供了一种用Kotlin编写前端应用的方式,支持React框架...
"kotlin最新版编译器"指的是Kotlin的最新版本的编译工具,用于将编写好的Kotlin源代码(.kt文件)转换为可执行的目标代码。 Kotlin 编译器(kotlinc)是这个过程的核心,它是Kotlin开发环境的关键组成部分。它负责...