一、函数式编程的核心思想

函数式编程,说白了就是一种编程范式。它强调用函数来表达一切逻辑。我刚开始接触 Kotlin 时,其实不太理解为什么要搞这么复杂。直到我在一个数据流处理项目中,被各种状态变更搞得焦头烂额,才真正体会到函数式编程的威力。

嗯,咱们今天要聊的这几个概念——纯函数、不可变性、函数组合、柯里化、Monad——它们不是孤立的。它们是一套完整的思维体系。你想想看,如果每个函数都像数学公式一样可靠,那调试起来得多省心?

1.1 纯函数

纯函数有两个特点:

  • 相同的输入,永远得到相同的输出
  • 没有副作用(不修改外部变量、不打印、不写文件)

举个例子:

// 纯函数
fun add(a: Int, b: Int): Int = a + b

// 不纯的函数
var counter = 0
fun impureAdd(a: Int, b: Int): Int {
    counter++  // 副作用:修改了外部变量
    return a + b
}

我在项目中遇到过这样的坑:一个看似简单的计算函数,内部偷偷修改了全局缓存。结果多线程环境下,数据全乱套了。从那以后,我写工具函数时都会问自己一句:「这个函数纯不纯?」

我的习惯:所有工具类函数,尽量写成纯函数。如果必须要有副作用,我会在函数名上加后缀,比如 saveToDb(),让调用者一眼就能看出来。

1.2 不可变性

不可变性,就是数据一旦创建,就不能再修改。Kotlin 用 valvar 来区分。

// 可变
var list = mutableListOf(1, 2, 3)
list.add(4)  // 直接修改原列表

// 不可变
val list2 = listOf(1, 2, 3)
val newList = list2 + 4  // 返回新列表,原列表不变

你可能会问:「每次都创建新对象,性能会不会很差?」

嗯,这个问题我当年也纠结过。实际上,Kotlin 的集合操作做了很多优化。比如 + 操作符,如果只是添加一个元素,底层会复用大部分数据。而且,不可变数据在并发场景下,根本不需要加锁。这个收益,远远大于那点性能损耗。

我曾经踩过的坑:在 RecyclerView 的 Adapter 中,直接修改了数据列表的元素。结果界面刷新时,因为引用没变,DiffUtil 检测不到变化。后来改用 copy() 创建新对象,问题就解决了。

二、函数组合与柯里化

2.1 函数组合

函数组合,就是把多个小函数拼成一个新函数。就像搭积木一样。

// 两个小函数
fun trim(s: String): String = s.trim()
fun toUpper(s: String): String = s.uppercase()

// 组合成一个新函数
val trimAndUpper: (String) -> String = { s -> toUpper(trim(s)) }

// 或者用扩展函数风格
infix fun <A, B, C> ((B) -> C).compose(f: (A) -> B): (A) -> C = { a -> this(f(a)) }

val composed = ::toUpper compose ::trim
println(composed("  hello  "))  // 输出 "HELLO"

我个人习惯把复杂的业务逻辑拆成多个小函数,然后用组合的方式串联起来。这样每个小函数都可以单独测试,出了问题也容易定位。

2.2 柯里化

柯里化,就是把一个多参数的函数,拆成一系列单参数的函数。Kotlin 原生不支持自动柯里化,但我们可以手动实现。

// 普通函数
fun add(a: Int, b: Int, c: Int): Int = a + b + c

// 柯里化版本
fun curriedAdd(a: Int): (Int) -> (Int) -> Int = { b -> { c -> a + b + c } }

// 使用
val add5 = curriedAdd(5)  // 固定第一个参数
val add5And3 = add5(3)    // 固定第二个参数
val result = add5And3(2)  // 输出 10

你想想看,柯里化的好处是什么?是「延迟求值」和「参数复用」。我在写配置类的时候经常用这个技巧。比如一个网络请求,先固定 baseUrl,再固定 token,最后只需要传具体接口参数就行了。

核心要点:柯里化让函数变得更灵活。你可以先「预制」一部分参数,剩下的参数等运行时再传入。

三、Monad 概念

Monad 这个词,听起来很高大上。其实说白了,它就是一种「包装类型」,帮你处理带有上下文的值。

Kotlin 中最常见的 Monad 就是 NullableResult。咱们拿 Nullable 举例:

// 没有 Monad 的时候
fun getUserName(userId: String): String? {
    val user = findUser(userId) ?: return null
    val profile = user.profile ?: return null
    return profile.name
}

// 用 Monad 的 flatMap(Kotlin 里叫 let)
fun getUserNameMonad(userId: String): String? {
    return findUser(userId)
        ?.let { it.profile }
        ?.let { it.name }
}

看到了吗?let 就是 Monad 的 flatMap 操作。它帮我们处理了「可能为空」这个上下文。如果中间某一步返回 null,后面的代码就不会执行了。

我在项目中用过 Arrow 库,它提供了更完整的 Monad 支持,比如 EitherOptionTry 等。不过说实话,对于大多数 Kotlin 项目,标准库自带的 letrunalso 已经够用了。

我的建议:不要为了用 Monad 而用 Monad。先理解它的思想——「把值包起来,然后通过一系列操作转换它」。等你遇到复杂的异步或错误处理场景时,自然会想到它。

四、知识体系总览

下面这张图,帮你理清今天讲的所有概念之间的关系:

函数式编程核心概念 纯函数 无副作用 · 引用透明 不可变性 val · copy() · 持久化 函数组合 compose · andThen 柯里化 参数复用 · 延迟求值 Monad flatMap · let · Either 核心思想:用函数表达一切,数据不可变,组合优于继承 纯函数 + 不可变数据 → 可预测 · 可测试 · 可并发

五、实战建议

说了这么多理论,咱们来点实际的。我在项目中总结了几条经验:

  1. 先从纯函数开始:写新函数时,先问自己「这个函数能不能写成纯函数?」能的话,就写成纯函数。
  2. 数据类用 val:Kotlin 的 data class 默认属性都是 val,这是有道理的。尽量别改成 var
  3. 用 let 代替 if-null:链式调用 ?.let 比写一堆 if (x != null) 要优雅得多。
  4. 不要过度设计:如果你的团队还不熟悉函数式编程,先引入纯函数和不可变性就够了。Monad 可以慢慢来。

一句话总结:函数式编程不是银弹,但它能帮你写出更可靠、更易测试的代码。从纯函数和不可变性开始,慢慢你就会发现,代码里的 bug 少了很多。

公众号:蓝海资料掘金营,微信 deep3321