函数式编程(0x2)

下一篇应该要很久了,由于个人原因,函数式编程的优先级降低,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。

结论

  1. (Haskell 中的)functor 是实现了 Functor 类型类的数据类型。
  2. (Haskell 中的)applicative 是实现了 Applicative 类型类的数据类型。
  3. (Haskell 中的)monad 是实现了 Monad 类型类的数据类型。
  4. Maybe 实现了这三者,所以它是 functor、 applicative、 以及 monad。

这三者有什么区别呢?

其实认真想一下:不懂这些概念能写出上面的代码吗?显然是可以的,就只是做了一些变换而已,看多了自然就会了。

相关推荐
王解36 分钟前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁41 分钟前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑2136 小时前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy6 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js