示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载本篇指南围绕 fpinscala 仓库 monads 章节第 20 道练习题展开逐行剖析 Reader Monad 的完整答案含ask、run与readerMonad实例并结合仓库源码解释其与 State Monad 的对称关系、只读环境透传的flatMap机制以及 Monad 律的测试验证。读完本文你将能独立实现 Reader Monad并理解它作为纯函数式依赖注入工具在配置读取、上下文共享等场景中的落地方式。习题背景monads 章节的第 20 道练习fpinscala 仓库按《Functional Programming in Scala》一书的章节组织练习README.md 明确列出书中第 11 章对应monads而本仓库中该章节的习题骨架位于 src/main/scala/fpinscala/exercises/monads/Monad.scala答案位于 src/main/scala/fpinscala/answers/monads/Monad.scala对应的提示与答案文档则是 answerkey/monads/20.hint.md 和 answerkey/monads/20.answer.md。本道练习的目标是为Reader类型实现一个Monad实例。习题骨架已经替你写好了类型别名和run扩展方法只留下unit与flatMap两个???待你填充opaque type Reader[-R, A] R A object Reader: extension R, A def run(r: R): A ra(r) given readerMonad[R]: Monad[Reader[R, _]] with def unitA: Reader[R, A] ??? extension A override def flatMapB ???也就是说Reader不过是一个环境R到结果A的函数给它一个只读环境它返回一个计算结果。先读 hintReader 与 State 的核心区别官方 hintanswerkey/monads/20.hint.md只用一句话点破了本质This monad is very similar to theStatemonad, except that its read-only. You can get but not set theRvalue thatflatMapcarries along.这句话拆开看有四个关键信息两者形态相似Reader与State都是函数包裹的 monadflatMap都负责把一个携带的上下文沿着计算链传递下去Reader 是只读的这个上下文环境R只能被读取不能被修改有get但没有setReader提供了类似State.get的ask用来取出环境但没有任何对应State.set/State.modify的写操作flatMap负责携带环境flatMap的签名决定了环境R如何被透传给每一步计算。换言之State 是可读写的环境每一步都可以读状态、写状态并把新状态传给下一步Reader 是只读的环境每一步都共享同一个环境且环境永不改变。这正是同一函数内核、两种不同语义的绝佳对照。参考 State同一内核的两种语义为了建立对照先看仓库中 State 的实现src/main/scala/fpinscala/answers/state/State.scalaopaque type State[S, A] S (A, S)State 是一个S (A, S)输入状态S输出结果A加上新状态S。因此它的flatMap必须把前一步产生的新状态作为下一步的输入def flatMapB: State[S, B] s val (a, s1) underlying(s) f(a)(s1)而 Reader 是R A输入环境R只输出结果A没有状态输出。正因为没有新环境这回事环境在整条计算链上是恒定不变的——这正是只读在类型层面的体现。从 State 延伸出的get/set/modify见 State.scaladef get[S]: State[S, S] s (s, s) def setS: State[S, Unit] _ ((), s) def modifyS: State[S, Unit] for s - get _ - set(f(s)) yield ()Reader 只保留了读的那一半即ask相当于只读版的get而把写的那一半set、modify彻底删除。答案文档 answerkey/monads/20.answer.md 给出的正是这样一个精简到极致的实现。完整答案逐行解析答案文档给出了 Reader monad 的完整实现object Reader: def ask[R]: Reader[R, R] r r extension R, A def run(r: R): A ra(r) given readerMonad[R]: Monad[Reader[R, _]] with def unitA: Reader[R, A] _ a extension A override def flatMapB r f(fa(r))(r)仓库 answers 版本src/main/scala/fpinscala/answers/monads/Monad.scala在此基础上补充了apply工厂方法和类型别名声明便于直接使用opaque type Reader[-R, A] R A object Reader: def ask[R]: Reader[R, R] r r def applyR, A: Reader[R, A] f extension R, A def run(r: R): A ra(r) given readerMonad[R]: Monad[Reader[R, _]] with def unitA: Reader[R, A] _ a extension A override def flatMapB r f(fa(r))(r)下面逐个成员拆解。opaque type Reader[-R, A] R Aopaque type是 Scala 3 的不透明类型别名对外它呈现为一个独立的抽象类型Reader[R, A]外界看不到它的底层表示在Reader的伴生对象内部它就被当成普通的R A函数来使用。这样设计的好处是用户无法绕过伴生对象直接对Reader做任意函数操作只能通过run、ask、flatMap等受控接口保证抽象边界编译期零开销Reader就是普通的函数值。变型标注[-R, A]也很有意思环境类型R是逆变的一个能处理更宽泛环境的Reader可以当作处理更具体环境的Reader使用结果类型A是协变的与List[A]等常见容器一致。ask只读版的getdef ask[R]: Reader[R, R] r rask返回一个把环境原样吐出来的Reader[R, R]等价于 State 中的gets (s, s)去掉状态包装后的形态。它是 Reader 唯一的读取环境入口在 for 推导式中写下env - Reader.ask就能把当前环境绑定到变量env上供后续计算使用。run执行环境extension R, A def run(r: R): A ra(r)run只是把底层的函数应用包装成方法调用reader.run(r)等价于reader(r)传入环境r得到结果A。它是整个 Reader 计算链的点火开关——所有惰性构建的flatMap组合只有在调用run的那一刻才会真正执行。这一点与State.run、Par.run的设计完全一致见 Par.scala 中extension A def run(s: ExecutorService): Future[A] pa(s)。unit忽略环境直接包装结果def unitA: Reader[R, A] _ aunit接收一个**按名传递by-name**的参数a返回一个忽略任何环境、直接给出a的 Reader。注意a的求值被推迟到run调用之后——这也是Monadtrait 中unitA的通用签名约定见 answers/monads/Monad.scala 第 2829 行保证了与LazyList、Gen等惰性 monad 的一致性。flatMap把同一个环境透传给每一步extension A override def flatMapB r f(fa(r))(r)这是整个练习的题眼逐步拆解fa(r)把环境r交给当前计算fa得到结果af(a)用结果a选出下一步计算得到一个Reader[R, B](r)把同一个环境r再传给下一步计算得到最终结果B。对比 State 的flatMapMonad底层类型flatMap 的环境处理State[S, A]S (A, S)把前一步的新状态s1传给下一步f(a)(s1)Reader[R, A]R A把同一个环境r传给每一步f(fa(r))(r)一句话概括State 的上下文像流水一样逐级传递并不断更新Reader 的上下文像阳光一样同时照亮所有步骤。这正是 hint 所说 flatMapcarries along theRvalue 的精确含义。从 Monad 基类自动获得的派生能力一旦readerMonad实现了unit与flatMapMonadtrait 中所有基于这两个原语的方法就全部免费获得见 answers/monads/Monad.scala 第 2875 行def mapB: F[B] fa.flatMap(a unit(f(a))) def map2B, C(f: (A, B) C): F[C] fa.flatMap(a fb.map(b f(a, b))) def sequenceA: F[List[A]] ... def traverseA, B(f: A F[B]): F[List[B]] ... def replicateMA: F[List[A]] ... def composeA, B, C: A F[C] ...代入 Reader 的具体形态可以直观感受到环境共享mapr f(fa(r))—— 环境只被读取一次f应用到结果上map2r f(fa(r), fb(r))——两个子计算接收同一个环境r。这正是 Reader 与 State 最显著的分野State 的map2会把第一个子计算的输出状态作为第二个子计算的输入见 answers/state/State.scala 中map2的for推导实现而 Reader 的map2让两个子计算并行共享环境sequence/traverse对整批 Reader 依次应用map2所有步骤同样共享同一个环境。也就是说一旦补全unit和flatMap你的 Reader 就自动成为一等公民的 Monad可以和sequence、traverse、replicateM等通用组合子协同工作。Monad 律与测试验证一个合法的 Monad 必须满足结合律与左右单位律。仓库的测试套件 src/test/scala/fpinscala/exercises/monads/MonadSuite.scala 对readerMonad做了完整验证test(readerMonad)(genInt ** genString): case n ** s assertMonad[Reader[Unit, _]](readerMonad, n, s)assertMonad依次检查unit、flatMap、map、map2四个基本行为第 162186 行而结合律、compose 结合律、左右单位律的检查函数assertAssociativeLaw、assertAssociativeCompose、assertIdentityLawForCompose、assertIdentityLawForFlatMap第 187208 行也一并就位。测试用Reader[Unit, _]作为被测实例因为Unit是唯一的环境值()比较两个 Reader 是否相等时只需各取run(())的结果private val readerMonad: TestedMonad[Reader[Unit, _]] new TestedMonad[Reader[Unit, _]]: val monad: Monad[Reader[Unit, _]] Reader.readerMonad[Unit] def pure[A]: A Reader[Unit, A] monad.unit override def assertFsA: Unit Assertions.assertEquals(actual.run(()), expected.run(()))这个测试设计也印证了两点一是Reader.readerMonad[R]是一个类型参数化的 given对任意环境类型R都能生成对应 Monad 实例即一族 monad二是 Reader 的相等性判定必须通过run显式提供环境来落地——这正体现了opaque type封装下函数值只能通过接口观察行为的特点。顺带一提readerMonad[R]: Monad[Reader[R, _]]中的Reader[R, _]是类型构造器的部分应用写法。answers 源码在stateMonad的注释中说明answers/monads/Monad.scala这类写法需要开启-Ykind-projector:underscores编译选项。实战场景纯函数式的依赖注入Reader 最常见的应用是把环境/配置/上下文以纯函数方式注入到计算中也就是函数式圈子常说的 Reader 模式。一个典型例子程序里有一段逻辑需要读取数据库连接、日志器、配置参数等环境但又不希望把这些环境参数显式地层层透传。// 环境一个假想的配置对象 case class Config(apiKey: String, baseUrl: String, timeoutSec: Int) // 用 Reader 表达需要环境才能计算的动作 val getKey: Reader[Config, String] Reader.ask.map(_.apiKey) val buildUrl: Reader[Config, String] for cfg - Reader.ask yield s${cfg.baseUrl}/v1?key${cfg.apiKey} // 组合两个动作共享同一个环境 val combined: Reader[Config, (String, String)] for key - getKey url - buildUrl yield (key, url) // 最后只需提供一个环境即可运行整条计算链 val result: (String, String) combined.run(Config(abc, https://api.example.com, 30))注意combined内部的每一步都拿到的是同一个Config实例环境在整个推导过程中恒定不变。若改用 State 表达同样的需求每一步都可能修改环境反而破坏了配置只读的语义。这正是可读写的环境交给 State、只读的环境交给 Reader的取舍依据。在仓库中继续探索的路径想进一步吃透本主题可以按以下路径在仓库中继续阅读answerkey/monads/20.hint.md本题官方提示一句话点明 Reader 与 State 的本质区别answerkey/monads/20.answer.md本题最小化答案src/main/scala/fpinscala/answers/monads/Monad.scala含ask、apply、run、readerMonad的完整实现以及Monadtrait 的全部派生方法src/main/scala/fpinscala/exercises/monads/Monad.scala习题骨架unit与flatMap留白待你练习src/main/scala/fpinscala/answers/state/State.scalaState monad 的完整实现用于与 Reader 逐行对照src/test/scala/fpinscala/exercises/monads/MonadSuite.scalareaderMonad的 Monad 律测试。完成unit与flatMap两个???后你可以按 README.md 中的说明运行测试验证正确性scala-cli test . -- fpinscala.exercises.monads.*。亲手写一遍这个答案再对照 State 的flatMap反复揣摩同构而不同义这一点你对 monad 本质的理解会上一个台阶。赞分享示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载相关推荐fpinscala 第 11 章练习 20 解答从零实现只读环境 Reader Monadfpinscala 第 11 章练习 20 解答从零实现只读环境 Reader Monad 本篇技术指南以 fpinscala 仓库中 monads 章节练习示例工程fpinscala 习题 20 精讲用 Traverse 实现 Monad 组合 composeMfpinscala 习题 20 精讲用 Traverse 实现 Monad 组合 composeM 本文围绕《Functional Programming i示例工程fpinscala Monad 习题精讲replicateM 组合子的两种实现与多 Monad 语义剖析fpinscala Monad 习题精讲replicateM 组合子的两种实现与多 Monad 语义剖析 本文以 fpinscala 仓库《Function示例工程上一篇Hyperswitch事件驱动支付事件异步处理下一篇dupeguru代码注释规范终极指南提升项目可读性的10个专业实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?