探讨函数式编程中的 Applicative Functor 泛式

四月 07, 2026 / Administrator / 51阅读 / 0评论/ 分类: Java

初探函数式编程(FP)时,我们经常会撞上一堵名为“范畴论”的术语高墙:Functor(函子)Applicative(应用函子)Monad(单子)……这些词汇初听起来极其晦涩,常常让人望而却步。

本文将以 Kotlin 为主语言,用通俗易懂的“盒子”比喻,从最基础的概念起步,带你拆解 Applicative Functor(应用函子) 设计模式。


一、 从“盒子”说起:什么是 Functor(函子)?

在讲 Applicative 之前,我们必须先理解 Functor。

在函数式编程中,我们经常把数据装进一个“盒子”(Wrapper/Context)里。例如处理可能为空的值、异步的结果等。 假设你有一个盒子,里面装着数字 2。现在你有一个普通的函数 加 3,你想让盒子里的值加上 3,但普通函数是无法直接作用于盒子的。

Functor,就是一个提供 map 方法的盒子。 map 的作用是:帮你把盒子打开,让函数对里面的值起作用,然后再把结果装回新的盒子中。

// 这是一个 Functor(盒子)
class Box<T>(val value: T) {

// 允许普通函数 (T) -> R 进来操作,并返回新的盒子 Box<R>
fun <R> map(transform: (T) -> R): Box<R> = Box(transform(value))
}

// 使用 Functor
val box = Box(2)
val newBox = box.map { it + 3 } // 结果:Box(5)

(注:在日常开发中,Java 的 OptionalStream,Kotlin 的 List 等,都具备 Functor 的特性。)


二、 Functor 的局限与 Applicative 的诞生

Functor 很好用,但它有一个致命的局限:它只能处理单参数函数。

假设我们有一个需要两个参数的函数:fun add(a: Int, b: Int) = a + b 现在有两个相互独立的盒子:Box(2)Box(3)。 你想把这两个盒子里的值加起来,单靠 Functor 的 map 是做不到的,因为它无法同时解开两个盒子进行运算。更何况,如果函数本身也是被装在盒子里的呢?

为了解决这种“多盒子、多参数”的组合问题,Applicative Functor(应用函子) 诞生了。 它是 Functor 的升级版,具备以下两个核心“超能力”:

  1. pure(或称 just):能把一个普通的任意值/函数,放到盒子里。

  2. ap(即 apply):能让“装在盒子里的函数” 去作用于 “装在盒子里的值”


三、柯里化(Currying)与 ap

这是 Applicative 最考验思维转换的部分。在实际工程中,我们常常使用封装好的 map2map3 来组合多个盒子,但其底层的理论基石,是柯里化(Currying) + 链式 ap

1. 什么是柯里化?

柯里化是一种拆分技术:把接收多个参数的函数,变成一系列每次只接收一个参数的函数。

当你传入第一个参数 2 时,它不会马上计算结果,而是返回一个记住了 2新函数(处于等待状态),直到你传入第二个参数 3,它才吐出最终结果 5

// 普通的加法函数:(Int, Int) -> Int
fun add(a: Int, b: Int) = a + b

// 柯里化后的加法函数:(Int) -> ((Int) -> Int)
val curriedAdd: (Int) -> ((Int) -> Int) = { a -> 
    { b -> a + b } 
}

2. 用 Kotlin 实现 ap

我们为 Box 添加 pureap 的能力,并使用 infix 关键字实现中缀调用:

class Box<T>(val value: T)

// pure:把任意东西装进盒子
fun <T> pure(value: T): Box<T> = Box(value)

// ap:把盒子里的函数,应用到另一个盒子里的值上
infix fun <A, B> Box<(A) -> B>.ap(boxA: Box<A>): Box<B> {
    val func = this.value   // 把函数从当前盒子里拿出来
    val a = boxA.value      // 把值从 boxA 拿出来
    val result = func(a)    // 执行计算
    return Box(result)      // 把结果装进新盒子返回
}

3. 链式调用

现在我们要把 Box(2)Box(3) 加起来。我们来看看基于柯里化和 ap 的执行推演:

val box2 = Box(2)
val box3 = Box(3)

// 非常地优雅!
val resultBox = pure(curriedAdd) ap box2 ap box3
println(resultBox.value) // 输出:5

推演拆解:

  1. pure(curriedAdd):将接收两个参数的柯里化函数装入盒子。此时盒子状态为:Box({ a -> { b -> a + b } })

  2. ... ap box2ap 打开两个盒子,让函数吃掉 2。返回的新函数只剩一个参数,被重新装入盒子。此时盒子状态变为:Box({ b -> 2 + b })

  3. ... ap box3:用刚刚得到的新盒子,继续 ap 装有 3 的盒子。函数吃掉 3,执行 2 + 3,最终返回 Box(5)

为什么我们要搞这么复杂...? 如果你定义 map2 来处理两个盒子,那 3 个盒子就需要写 map3,10 个参数就需要 map10。 但如果使用“柯里化 + ap”,无论多少个参数,规则永远是统一的:pure(func) ap box1 ap box2 ... ap boxN。一组极其简单的基础规则,足以解决任意复杂度的参数传递!


四、 表单校验功能实践

Applicative 的一个核心特质是:它处理的多个盒子之间是相互独立的(比如并行的异步网络请求)。这与 Monad 的串行依赖(后一个操作依赖前一个结果)有着本质区别。这一特质,让 Applicative 成为了处理类似表单校验(错误累积)的绝佳方案。

业务痛点:

在用户注册页面,我们需要校验 用户名邮箱年龄。传统的 Fail-fast 校验查到一个错误就直接抛异常,当用户存在多种不同类型的错误时,用户体验极差。我们需要一次性校验所有字段,并将错误全部收集汇总

代码实战:使用 Validation Applicative

我们定义一个代表校验结果的盒子 Validation:要么是有效的 Valid,要么是包含错误列表的 Invalid

// 验证类
sealed class Validation<out E, out A> {
    data class Valid<A>(val value: A) : Validation<Nothing, A>()
    data class Invalid<E>(val errors: List<E>) : Validation<E, Nothing>()
}

// 将三个独立的 Validation 组合起来。
// 规则:如果全部 Valid,则执行合并逻辑;只要有 Invalid,就累加所有的错误列表!
fun <E, A, B, C, R> map3(
    v1: Validation<E, A>,
    v2: Validation<E, B>,
    v3: Validation<E, C>,
    f: (A, B, C) -> R
): Validation<E, R> {
    val errors = mutableListOf<E>()
    
    // 尝试提取值,若失败则收集错误
    val a = if (v1 is Validation.Valid) v1.value else { errors.addAll((v1 as Validation.Invalid).errors); null }
    val b = if (v2 is Validation.Valid) v2.value else { errors.addAll((v2 as Validation.Invalid).errors); null }
    val c = if (v3 is Validation.Valid) v3.value else { errors.addAll((v3 as Validation.Invalid).errors); null }

    return if (errors.isEmpty()) {
        Validation.Valid(f(a!!, b!!, c!!))
    } else {
        Validation.Invalid(errors)
    }
}

业务调用:

data class User(val name: String, val email: String, val age: Int)

fun validateName(name: String) = if (name.isNotBlank()) Validation.Valid(name) else Validation.Invalid(listOf("用户名不能为空"))
fun validateEmail(email: String) = if (email.contains("@")) Validation.Valid(email) else Validation.Invalid(listOf("邮箱格式错误"))
fun validateAge(age: Int) = if (age >= 18) Validation.Valid(age) else Validation.Invalid(listOf("未满18岁"))

fun main() {
    // 模拟非法的用户输入
    val result = map3(
        validateName(""), 
        validateEmail("no-at"), 
        validateAge(25)
    ) { name, email, age -> 
        User(name, email, age) 
    }

    when (result) {
        is Validation.Valid -> println("注册成功: ${result.value}")
        is Validation.Invalid -> println("校验失败,错误汇总: ${result.errors}")
    }
}

// 输出结果: "校验失败,错误汇总: [用户名不能为空, 邮箱格式错误]"

#Kotlin(1)

文章作者:Administrator

文章链接:https://www.catnies.top/archives/wei-ming-ming-wen-zhang

版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0 许可协议,转载请注明出处!


评论