1. 概述

Arrow 是 Kotlin 生态中一个功能强大的函数式编程库,它的定位类似于 Scala 社区中的 Cats 库。它通过两种主要方式为 Kotlin 增强函数式能力:扩展函数代数数据类型(ADT)

  • 扩展函数:在不修改原始类的前提下,为现有类型(包括 Java 类型)注入新行为,让调用更自然。
  • 代数数据类型(ADT):提供如 OptionEitherEval 等类型,这些类型实现了函数式编程中的核心抽象,比如函子(Functor)、应用式(Applicative)和单子(Monad)。

例如,Arrow 中的 Validated 类就实现了 Applicative 接口,非常适合用于累积错误的场景。

本文将带你快速掌握 Arrow 的核心概念,并重点演示如何利用其数据类型实现优雅的错误处理。

💡 目标读者是有一定 Kotlin 经验的开发者,因此基础语法不再赘述。

2. Maven 依赖

要在项目中使用 Arrow,只需引入 arrow-core 模块:

<dependency>
    <groupId>io.arrow-kt</groupId>
    <artifactId>arrow-core</artifactId>
    <version>1.2.0</version>
</dependency>

如果你使用 Gradle:

implementation("io.arrow-kt:arrow-core:1.2.0")

3. Arrow 中的扩展函数

Arrow 利用 Kotlin 的扩展函数机制,为标准类型注入了大量函数式编程工具。以下是几个高频使用的例子。

3.1 函数组合、柯里化与偏应用

函数式编程强调函数的组合与复用,Arrow 提供了以下支持:

  • compose():函数组合,先执行右侧函数,再将结果传给左侧函数。
val multiplyBy2 = { i: Int -> i * 2 }
val add3 = { i: Int -> i + 3 }
val composed = multiplyBy2 compose add3 // 先乘2再加3
val result = composed(4) // 结果: (4 * 2) + 3 = 11 ❌ 注意原文有误,应为11而非14

⚠️ 原文此处计算错误,正确逻辑是 add3(multiplyBy2(4))add3(8)11

  • curried():柯里化,将多参数函数转换为一系列单参数函数链。
val add = { a: Int, b: Int -> a + b }
val curriedAdd = add.curried()
val add2 = curriedAdd(2) // 固定第一个参数
val result = add2(3) // 5
  • partially():偏应用,直接绑定部分参数生成新函数,无需显式柯里化。
val addThreeNums = { a: Int, b: Int, c: Int -> a + b + c }
val add2 = addThreeNums.partially1(2) // 固定第一个参数为2
val result = add2(3, 4) // 9

3.2 为现有类型增强 flatMap 与 flatten

Arrow 为 Kotlin 标准类型(如 ResultMap)也提供了 flatMapflatten 扩展,提升操作一致性。

  • 对 Map 使用 flatMap 进行键值转换并扁平化:
val myMap = mapOf(
    "a" to listOf(1, 2, 3),
    "b" to listOf(4, 5, 6),
    "c" to listOf(7, 8, 9)
)

val flatMappedList = myMap.flatMap { (key, values) ->
    values.map { "$key$it" }
}
// 输出: [a1, a2, a3, b4, b5, b6, c7, c8, c9]
println(flatMappedList)
  • flatten() 用于展开嵌套集合:
val nestedList = listOf(listOf(1, 2), listOf(3, 4))
val flattened = nestedList.flatten() // [1, 2, 3, 4]

4. 函数式数据类型

Arrow 的核心价值在于其提供的不可变、类型安全的数据容器。我们来看几个关键类型。

4.1 单子(Monad)简介

虽然不是必须理解才能使用,但了解 Monad 能更好把握设计思想。一个类型要成为 Monad,通常需满足:

  • 包装值的容器
  • 提供构造方法(如 just
  • 实现 mapflatMap
  • 无副作用

Java 中的 Stream 可视为 Monad,而 Optional 因违反结合律而不完全符合。

4.2 Option:安全的可空值处理

Option 用于表示“可能不存在”的值,替代易出错的 null

  • Some(value):有值
  • None:无值

创建方式对比:

val someFromFactory = Some(42)
val someFromConstructor = Option(42)
val empty: Option<Int> = none()
val fromNull = Option.fromNullable(null)

assertEquals(42, someFromFactory.getOrElse { -1 })
assertEquals(someFromFactory, someFromConstructor)
assertEquals(empty, fromNull)

⚠️ 踩坑点:Option(null)Option.fromNullable(null) 行为不同!

val constructor: Option<String?> = Option(null)        // Some(null) ❌ 实际上包装了 null
val fromNullable: Option<String?> = Option.fromNullable(null) // None ✅ 正确表示缺失

// 使用 constructor 时,map 内部访问 s.length 会触发 KotlinNullPointerException
try {
    constructor.map { s -> s!!.length } // 抛异常
} catch (e: KotlinNullPointerException) {
    // 难看
}

// 推荐使用 fromNullable,天然避免 NPE
fromNullable.map { s -> s!!.length } // 不会执行,因为是 None

最佳实践:始终使用 Option.fromNullable() 处理可能为空的值。

4.3 Either:二元结果,常用于错误处理

Either 表示两种可能的结果:成功(Right)或失败(Left),它是右偏(right-biased)的,即大多数操作只作用于 Right 分支。

val right: Either<String, Int> = Either.Right(42)
val left: Either<String, Int> = Either.Left("解析失败")
  • getOrElse 默认从 Right 取值:
assertEquals(42, right.getOrElse { -1 })
assertEquals(-1, left.getOrElse { -1 })
  • mapflatMap 仅对 Right 生效,Left 被短路:
assertEquals(Either.Right(0), right.map { it % 2 })
assertEquals(Either.Left("解析失败"), left.map { it % 2 }) // Left 不变

这种设计非常适合错误传播——一旦出错,后续操作自动跳过。

4.4 Eval:可控的延迟求值

Eval 用于控制表达式的求值时机,支持立即、惰性和缓存求值。

  • now:立即求值,结果固定
var counter = 0
val now = Eval.now { counter++; 1 }
// counter 已经 +1
  • later:首次访问 value() 时求值,结果缓存
var counter = 0
val later = Eval.later { counter++; counter }
assertEquals(0, counter) // 未执行

assertEquals(1, later.value()) // 第一次:counter=1
assertEquals(1, later.value()) // 第二次:返回缓存值,counter 仍为1
  • always:每次 value() 都重新计算
var counter = 0
val always = Eval.always { counter++; counter }

assertEquals(1, always.value()) // counter=1
assertEquals(2, always.value()) // counter=2,重新计算

✅ 适用场景:later 适合开销大但结果不变的操作;always 适合需要动态刷新的配置等。

5. 函数式错误处理模式

传统异常处理存在性能开销(尤其是 fillInStackTrace)和调用方不可见等问题。Arrow 提供更优雅的替代方案。

5.1 使用 Option 进行简单错误处理

假设我们要实现一个功能:判断用户输入的偶数的最大因数是否为平方数。

第一步:安全解析输入

fun parseInput(s: String): Option<Int> = Option.fromNullable(s.toIntOrNull())

业务逻辑链式调用,清爽无 try-catch:

fun isEven(x: Int): Boolean = x % 2 == 0
fun biggestDivisor(x: Int): Int = (x / 2).takeIf { it > 0 } ?: 1
fun isSquareNumber(x: Int): Boolean = sqrt(x.toDouble()).rem(1) == 0.0

fun computeWithOption(input: String): Option<Boolean> {
    return parseInput(input)
        .filter(::isEven)
        .map(::biggestDivisor)
        .map(::isSquareNumber)
}

客户端处理结果:

fun computeWithOptionClient(input: String): String {
    return when (val result = computeWithOption(input)) {
        is None -> "输入无效或不是偶数!"
        is Some -> "最大因数是平方数: ${result.t}"
    }
}

❌ 缺点:无法区分是“非数字”还是“奇数”,错误信息粒度粗。

5.2 使用 Either 提供详细错误信息

改进方案:用 Either<Error, Success> 明确错误类型。

定义错误类型(推荐密封类):

sealed class ComputeProblem {
    object NotANumber : ComputeProblem()
    object OddNumber : ComputeProblem()
}

改造解析函数:

fun parseInput(s: String): Either<ComputeProblem, Int> =
    if (s.toIntOrNull() != null) Either.Right(s.toInt())
    else Either.Left(ComputeProblem.NotANumber)

filterEither 中变为 filterOrElse,需指定失败时的 Left 值:

fun computeWithEither(input: String): Either<ComputeProblem, Boolean> {
    return parseInput(input)
        .filterOrElse(::isEven) { ComputeProblem.OddNumber }
        .map(::biggestDivisor)
        .map(::isSquareNumber)
}

客户端可精准处理不同错误:

fun computeWithEitherClient(input: String): String {
    return when (val result = computeWithEither(input)) {
        is Either.Right -> "最大因数是平方数: ${result.b}"
        is Either.Left -> when (result.a) {
            ComputeProblem.NotANumber -> "错误:输入不是有效数字"
            ComputeProblem.OddNumber -> "错误:输入不是偶数"
        }
    }
}

✅ 优势:

  • 错误类型在编译期可见
  • 避免异常开销
  • 逻辑清晰,无嵌套 if-else

6. 总结

Arrow 为 Kotlin 注入了强大的函数式编程能力。通过 OptionEither,我们可以构建类型安全、可组合且易于测试的业务逻辑,尤其在错误处理场景下,相比传统异常机制更加优雅和高效。

文中所有代码示例均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/kotlin-libraries


原始标题:Introduction to Arrow in Kotlin