下一篇应该要很久了,由于个人原因,函数式编程的优先级降低,jvm的优先级上升。
上一篇我们说过,函数是一个很简单的东西:
你给它一个input,它给你一个output,且不会对function的外界产生影响。
柯里化(Curried Functions)
柯里化是一个数学概念, 简单的来说,就是对于一个有多个参数的函数,转换成每次只接受一个参数的函数,最后输入结果。
很多文章在讲柯里化的时候,会写这样的一个例子:
kotlin
fun add(x: Int, y: Int): Int {
return x + y
}
这是一个普通的函数,对这个函数进行柯里化变化后:
kotlin
fun addCurried(x: Int): (Int) -> Int {
return { y -> x + y }
}
可以看出函数发生了两个变化:
- 函数只需要一个输入参数
- 函数返回了另外一个函数
在kotlin里面,我们在add_curried的函数体里面创建了另一个函数体,并将它返回,这样我们使用的时候就可以:
kotlin
fun main(args: Array<String>) {
// 输出结果 8
println(addCurried(3)(5))
}
这确实是一个很简单的例子,但是却不太符合常理。我第一眼看到这个例子,大概知道了柯里化是咋回事,但是还是会吐槽:这特么的有个屌用!
所以我们来讨论一个更加实际的例子:
我们要实现一个取款流程的逻辑,用户插入一张银行卡,然后输入密码,最后输入要取得钱的数量。一般情况下,我们可能这样设计:
kotlin
class Card
class CM {
private var card:Card? = null
private var password:String? = null
private var request:Int? = null
fun withdraw(card: Card, password:String, request:Int) {
}
}
使用一些成员变量将用户的输入都储存起来,然后等到用户点击取款的按钮的时候,再触发真正的取款函数。我们选择这样做的一个很大原因是因为用户的输入不是一次性,而是分时进行的,所以需要一个地方将用户的输入储存起来。
但是,有了柯里化思想之后,我们就可以这样写代码:
kotlin
fun withdraw(card: Card): (String) -> ((Int) -> Unit) {
return { password ->
{ request ->
println("$card, $password, $request")
}
}
}
这样使用:
ini
val cardStep = CMCurried().withdraw(Card())
val pwStep = cardStep("password")
val done = pwStep(100)
希望,这个例子能让你对柯里化有个正确的理解,而且你也应该注意到这样有个暗藏的好处:pwStep 可以重复利用,而且不需要重复的输入 card 参数,除非你的 card 参数值变化了。
当然,你也可能会觉得这也没啥,和Builder模式不是同样的道理嘛!前文说过了,fp 与 oop 走的是不同的道路,但是目的是一样的。看使用哪种更合适,由你自己决定。
functor
之所以要写一个英文标题,是不知道该怎么翻译这个玩意,有的文章叫它函子 或者仿函数,反正我是搞不懂这两个名词说的是啥玩意,情愿不翻译。
我们从一个简单的例子说起,java里面有类类型与值类型,通常函数都是对值类型做操作:
上图中,函数 f(x) = x + 3
,输入值 2,返回值 5。但是这是比较理想的情况,通常我们使用值类型都是一些对象的字段,比如,有一个 Functor 类,它有一个 value 字段,我们要操作的就是 value 字段。我们将 value 所在的 Functor 类叫做 value 的上下文。
kotlin
class Functor(val value: Int) {
}
问题就在于,函数无法接受一个 Functor 类,所以我们想到一个办法,给 Functor 添加一个扩展函数:
kotlin
fun Functor.fmap(transform: (Int) -> Int): Functor {
return Functor(transform(this.value))
}
先解开 Functor 类,拿到里面的 value 变量,再对该变量做 transform,然后在包装回去:
我们称 Functor 这种类叫做 Functor。
在 Kotlin 中,可以认为
Functor
是一种定义了类似fmap
方法/扩展函数的类型。
或者说的更直接一点,当你相对一个对象的字段做操作的时候,这个类就可以变成一个 Functor。嗯,好像有点大忽悠的意思,反正自己悟吧。
还有一个神奇的用法:
kotlin
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)
将函数当作Functor,做成组合的形式,看着很绕,其实就是对输入做了两次函数处理,this 一次,transform 一次,所以最后的结果是 15。
Applicative
Applicative 相比 Functor,稍微牛逼了一点。因为 Functor 是将值变量包装了一层,但是 Applicative 是将函数包装了一层:
Functor:
Applicative:
我们将上面的代码稍微改以下,增加一个泛型:
kotlin
class Applicative<T>(val value: T) {
}
fun <T, R > Applicative<T>.fmap(transform: (T) -> R): Applicative<R> {
return Applicative(transform(this.value))
}
fun <T, R > Applicative<(T) -> R>.combine(applicative: Applicative<T>): Applicative<R> {
return applicative.fmap(this.value)
}
这个时候,T 也可以表示函数类型,那么我们就可以这样使用:
kotlin
fun main() {
val applicative = Applicative { x: Int -> x + 3 }.combine(Applicative(2))
println(applicative.value)
}
可以看到,其实与 Functor 是一样的代码,只不过类型从 Int 变成了 (T) → R。
Applicative 主要是做这样的一个事情:将一个包装在上下文中的函数应用到一个包装在上下文中的值上。
Monad
Monad 将一个返回已包装值的函数应用到一个已包装的值上。
看例子:
kotlin
sealed class Maybe<out T> {
object `Nothing#` : Maybe<Nothing>() {
override fun toString(): String = "Nothing#"
}
data class Just<out T>(val value: T) : Maybe<T>()
}
我们定义了一个 Maybe 类,它有两种形式:just 与 Nothing#。
有这样的一个函数,当输入是偶数时减半,否则返回 Nothing#:
kotlin
fun half(x: Int) = if (x % 2 == 0)
Maybe.Just(x / 2)
else
Maybe.`Nothing#`
如果我们想利用这个函数直接对 Maybe 对象操作,肯定是不行的,所以我们可以加一个扩展函数,或者是搞个操作符:
kotlin
infix fun <T, R> Maybe<T>.`))=`(f: ((T) -> Maybe<R>)): Maybe<R> = when(this) {
Maybe.`Nothing#` -> Maybe.`Nothing#`
is Maybe.Just -> f(this.value)
}
这里我们定义了一个操作符 ))=
,它其实就是利用了参数的变化,将 T 变成一个 Maybe 类型,这刚好与 half 类型相符合。其实这个操作符就是简单的做了一个解包的操作!
我们还可以这样使用:
kotlin
fun main() {
Maybe.Just(20) `))=` ::half `))=` ::half `))=` ::half
val just = (Maybe.Just(20) `))=` ::half) as Maybe.Just<Int>
println(just.value)
}
第一个输出 Nothing#,第二个输出10。
结论
- (Haskell 中的)functor 是实现了
Functor
类型类的数据类型。 - (Haskell 中的)applicative 是实现了
Applicative
类型类的数据类型。 - (Haskell 中的)monad 是实现了
Monad
类型类的数据类型。 Maybe
实现了这三者,所以它是 functor、 applicative、 以及 monad。
这三者有什么区别呢?
其实认真想一下:不懂这些概念能写出上面的代码吗?显然是可以的,就只是做了一些变换而已,看多了自然就会了。