函数式里的依赖注入:Reader Monad
经常搞六边形架构的人也知道,为了保持核心业务逻辑的纯粹和独立,我们会把像数据库、外部 API 调用这些“副作用”放在“端口”和“适配器”里,然后通过 DI 的方式注入到应用层。可以说,经典的面向对象和分层架构,离不开 DI。
然后,当我想在 MoonBit 里做点事情的时候,我发现我不能呼吸了。
我们也想讲究一个入乡随俗,但是在 moonbit 这种函数味儿很浓郁的场地,没有类,没有接口,更没有我们熟悉的那一套 DI 容器。那我怎么做 DI?
我当时就在想,软件工程发展到至今已经约 57 年,真的没有在函数式编程里解决 DI 的方法吗?
有的兄弟,有的。只是它在函数式编程里也属于一种 monad:Reader Monad
什么是 Monad
普通的函数就像一个流水线,你丢进去一袋面粉,然后直接跑到生产线末端,等着方便面出来。但这条流水线需要自动处理中间的所有复杂情况:
- 没放面粉/“没有下单,期待发货”(null)
- 面团含水量不够把压面机干卡了(抛出异常)
- 配料机需要读取今天的生产配方,比如是红烧牛肉味还是香菇炖鸡味(读取外部配置)
- 流水线末端的打包机需要记录今天打包了多少包(更新计数器)
Monad 就是专门管理这条复杂流水线的“总控制系统”。它把你的数据和处理流程的上下文一起打包,确保整个流程能顺畅、安全地进行下去。
在软件开发中,Monad 这一家子有几个常见的成员:
- Option:处理“可能没有”的情况。盒子里要么有东西,要么是空的
- Result:处理“可能会失败”的情况。盒子要么是绿的(成功),里面装着结果;要么是红的(失败),里面装着错误信息
- State Monad:处理“需要修改状态”的情况。这个盒子在产出结果的同时,还会更新盒子侧面的一个计数器。或者说就是 React 里的
useState - Future(Promise):处理“未来才有”的情况。这个盒子给你一张“提货单”,承诺未来会把货给你
- Reader Monad: 盒子可以随时查阅“环境”,但不能修改它
Reader Monad
Reader Monad 的思想,最早可以追溯到上世纪90年代,在 Haskell 这种纯函数式编程语言的圈子里流行起来。当时大家为了坚守“函数纯度”这个铁律(即函数不能有副作用),就必须找到一种优雅的方式来让多个函数共享同一个配置环境,Reader Monad 就是为了解决这个矛盾而诞生的。
如今,它的应用场景已经非常广泛:
- 应用配置管理:用来传递数据库连接池、API密钥、功能开关等全局配置
- 请求上下文注入:在 Web 服务中,把当前登录的用户信息等打包成一个环境,供请求处理链上的所有函数使用
- 实现六边形架构:在六边形(或端口与适配器)架构中,它被用来在核心业务逻辑(Domain/Application Layer)和外部基础设施(Infrastructure Layer)之间建立一道防火墙
简单来说,Reader Monad 就是一个专门处理只读环境依赖的工具。它要解决的就是这些问题:
- 参数钻孔 (Parameter Drilling):我们不想把一个 Properties 层层传递
- 逻辑与配 置解耦:业务代码只关心“做什么”,而不用关心“配置从哪来”。这使得代码非常干净,且极易测试
核心方法
一个 Reader 库通常包含以下几个核心部分。
Reader::pure
就像是把一颗糖直接放进一个标准的午餐盒里。它把一个普通的值,包装成一个最简单的、不依赖任何东西的 Reader 计算。
pure 通常是流水线的打包机,它把你计算出的最终结果(一个普通值)重新放回 Reader “流水线”上,所谓“移除副作用”。
typealias @reader.Reader
// `pure` 创建一个不依赖环境的计算
let ?
pure_reader : Reader[String
String, Int
Int] = (Int) -> ?
Reader::(Int) -> ?
pure(100)
test {
// 无论环境是什么 (比如 "hello"),结果都是 100
fn[T : Eq + Show] assert_eq(a : T, b : T, msg? : String, loc~ : SourceLoc = _) -> Unit raise
Asserts that two values are equal. If they are not equal, raises a failure
with a message containing the source location and the values being compared.
Parameters:
a : First value to compare.
b : Second value to compare.
loc : Source location information to include in failure messages. This is
usually automatically provided by the compiler.
Throws a Failure error if the values are not equal, with a message showing
the location of the failing assertion and the actual values that were
compared.
Example:
test {
assert_eq(1, 1)
assert_eq("hello", "hello")
}
assert_eq(let pure_reader : ?
pure_reader.(String) -> Int
run("hello"), 100)
}
Reader::bind
这是流水线的“连接器”。例如把“和面”这一步和“压面”这一步连接起来,并确保它们能连成一条“生产线”。
为什么需要它? 为了自动化! 。bind 让这个过程全自动,你只管定义好每个步骤,它负责传递。
fnalias () -> ?
@reader.ask
// 步骤1: 定义一个 Reader,它的工作是从环境(一个Int)中读取值
let ?
step1 : Reader[Int
Int, Int
Int] = let ask : () -> ?
ask()
// 步骤2: 定义一个函数,它接收一个数字,然后返回一个新的 Reader 计算
fn fn step2_func(n : Int) -> ?
step2_func(Int
n : Int
Int) -> Reader[Int
Int, Int
Int] {
(Int) -> ?
Reader::(Int) -> ?
pure(Int
n fn Mul::mul(self : Int, other : Int) -> Int
Multiplies two 32-bit integers. This is the implementation of the *
operator for Int.
Parameters:
self : The first integer operand.
other : The second integer operand.
Returns the product of the two integers. If the result overflows the range of
Int, it wraps around according to two's complement arithmetic.
Example:
test {
inspect(42 * 2, content="84")
inspect(-10 * 3, content="-30")
let max = 2147483647 // Int.max_value
inspect(max * 2, content="-2") // Overflow wraps around
}
* 2)
}
// 使用 bind 将两个步骤连接起来
let ?
computation : Reader[Int
Int, Int
Int] = let step1 : ?
step1.((Int) -> ?) -> ?
bind(fn step2_func(n : Int) -> ?
step2_func)
test {
// 运行整个计算,环境是 5
// 流程: step1 从环境得到 5 -> bind 把 5 交给 step2_func -> step2_func 计算 5*2=10 -> pure(10)
fn[T : Eq + Show] assert_eq(a : T, b : T, msg? : String, loc~ : SourceLoc = _) -> Unit raise
Asserts that two values are equal. If they are not equal, raises a failure
with a message containing the source location and the values being compared.
Parameters:
a : First value to compare.
b : Second value to compare.
loc : Source location information to include in failure messages. This is
usually automatically provided by the compiler.
Throws a Failure error if the values are not equal, with a message showing
the location of the failing assertion and the actual values that were
compared.
Example:
test {
assert_eq(1, 1)
assert_eq("hello", "hello")
}
assert_eq(let computation : ?
computation.(Int) -> Int
run(5), 10)
}