fpinscala 第 11 章练习 20 解答:从零实现只读环境 Reader Monad

发布时间:2026/10/12 2:07:25
fpinscala 第 11 章练习 20 解答:从零实现只读环境 Reader Monad 示例工程【免费下载链接】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 的官方答案 为核心骨架完整讲解如何在 Scala 3 中为「只读环境」类型Reader手工实现一个合法的Monad实例。文章将逐行拆解ask、run、unit与flatMap的实现原理并结合仓库内的练习版源码、参考答案完整源码与属性测试用例说明 Reader 与 State Monad 的本质差异只读、可 get 不可 set最终给出可直接在scala-cli中运行验证的依赖注入式代码示例。一、练习背景Monad 章节中的「只读」变体fpinscala 是《Functional Programming in Scala》一书的配套开源仓库其中answerkey/monads/目录按题号存放了第 11 章Monads全部练习的提示与官方答案。练习 20 的目标是为Reader类型实现 Monad 实例。练习对应的提示文件 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与第 6 章实现的State高度相似——两者都会在flatMap的链条中「携带」一个额外值区别在于State允许读取并改写该状态get/set而Reader只允许读取环境R不允许修改。正是因为这种「只读」约束Reader被广泛用于依赖注入dependency injection、配置传递、作用域共享等场景。二、类型定义opaque type Reader[-R, A] R A答案版在 src/main/scala/fpinscala/answers/monads/Monad.scala#L172 中通过opaque type定义 Readeropaque type Reader[-R, A] R A这条定义包含三层含义本质是一个函数Reader[R, A]在底层就是R A——给定一个环境R返回一个结果A。所有「读取环境」的能力都来自这层函数抽象。opaque type不透明类型对外隐藏底层实现调用方无法直接把它当作函数R A使用必须通过Reader对象提供的run/ask等构造与访问接口从而保证抽象边界不被破坏。逆变-R与协变A环境类型R是「输入」声明为逆变-R结果类型A是「输出」声明为协变A。这与函数类型R A本身的型变方向一致。练习版骨架位于 src/main/scala/fpinscala/exercises/monads/Monad.scala#L123同样以opaque type Reader[-R, A] R A起头但unit与flatMap均为???等待读者自行补全——官方答案正是本题的标准解。三、ask与run读取环境的两把钥匙答案在object Reader中提供了两个核心成员object Reader: def ask[R]: Reader[R, R] r r extension R, A def run(r: R): A ra(r)ask[R]: Reader[R, R]返回「把环境原样交出来」的 Reader。它等价于恒等函数r r是 Reader 世界里的「读当前环境」原语——正如提示所说Reader 可以「get」但不能「set」。结合后面定义的 Monad 实例ask能在for推导中把当前环境绑定到变量上。extension ... def run(r: R): Arun是执行 Reader 的唯一入口。由于opaque type隐藏了底层函数结构外部必须调用reader.run(config)传入具体环境才能拿到结果A。注意参考答案中还额外定义了applyR, A: Reader[R, A] f作为从普通函数构造 Reader 的便捷方式Monad.scala#L176。四、Monad 实例unit与flatMap的逐行推导练习 20 的核心任务是补全given readerMonad[R]: Monad[Reader[R, _]]。官方答案given readerMonad[R]: Monad[Reader[R, _]] with def unitA: Reader[R, A] _ a extension A override def flatMapB r f(fa(r))(r)4.1unit把纯值封装进「忽略环境」的 Readerdef unitA: Reader[R, A] _ a参数a: A是按名by-name传参与Monadtrait 中def unitA: F[A]的签名保持一致见 Monad.scala#L29意味着该值到真正求值前不会被计算保留惰性。实现体_ a表示无论传入什么环境R都返回a。这是 Reader 的「纯」操作——不依赖、不修改环境。4.2flatMap串联两个「读环境」的计算extension A override def flatMapB r f(fa(r))(r)逐层拆解这个最关键的实现fa是一个Reader[R, A]即函数R Af: A Reader[R, B]接收上一步结果产出下一步 ReaderflatMap的结果仍是Reader[R, B]即函数R B因此整体写成r ...内部先执行fa(r)得到A再把它交给f得到Reader[R, B]最后再次传入同一个环境rf(fa(r))(r)。关键点在于最后一步f产出的 Reader 依然用原始的、未变化的环境r来运行。对比State的flatMapanswers/state/State.scala#L161-L164// State.flatMap —— 环境状态会从前一步的返回值中更新 s val (a, s1) underlying(s) f(a)(s1)State 把s1更新后的状态传给下一步而 Reader 的flatMap自始至终只使用同一个r。这正是「只读」在 Monad 层面的精确体现环境可以被读取无数次但永远不会被改写。4.3 类型构造的细节Monad[Reader[R, _]]Reader是二元类型构造器Reader[R, A]而Monad[F[_]]只接受一元类型构造器因此需要把R部分应用掉。答案使用Reader[R, _]占位语法kind-projector 下划线风格让F Reader[R, _]成为一个合法的Monad实例这与同一文件中State的处理方式完全一致——仓库在 Monad.scala#L138-L144 处注释说明借助-Ykind-projector:underscoresscalac 选项可以写出given stateMonad[S]: Monad[State[S, _]]这样的「一族」实例。五、练习版与答案版对照练习版 src/main/scala/fpinscala/exercises/monads/Monad.scala#L129-L133 中保留了相同骨架仅将实现挖空given readerMonad[R]: Monad[Reader[R, _]] with def unitA: Reader[R, A] ??? extension A override def flatMapB ???读者完成练习后将???分别替换为答案版中的_ a与r f(fa(r))(r)即可。完整的答案版实现位于 Monad.scala#L172-L185包含opaque type、object Readerask、apply、run与readerMonad三部分。六、测试验证Reader Monad 通过全部 Monad 法则断言仓库在 src/test/scala/fpinscala/exercises/monads/MonadSuite.scala#L158-L160 中为 Reader 注册了属性测试test(readerMonad)(genInt ** genString): case n ** s assertMonad[Reader[Unit, _]](readerMonad, n, s)该测试使用Reader[Unit, _]——即环境为Unit的 Reader运行时不携带任何实际配置通过assertMonad依次断言四条性质MonadSuite.scala#L162-L185assertUnitmonad.unit(n)等价于pure(n)assertFlatMapflatMap(unit(n))(i unit(i % 2 0))结果等于pure(n % 2 0)assertMapmap(unit(n))(_ % 2 0)结果一致assertMap2map2(unit(n))(unit(s))((_, _))能组合出二元组。值得注意TestedMonad[Reader[Unit, _]]的定制断言MonadSuite.scala#L263-L268private 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底层是函数不透明类型两个 Reader 无法直接比较相等性因此assertFs统一调用run(())把双方“跑”出具体结果后再做assertEquals。这提示了一个使用约定凡是比较 Reader都必须先run。在仓库根目录执行以下命令即可运行整个 monads 测试套件命令来源于 README.mdscala-cli test . -- fpinscala.exercises.monads.*七、实战示例用 Reader 做依赖注入与配置传递基于官方答案我们可以立刻把 Reader 用于典型的「配置注入」场景。假设环境为数据库配置import fpinscala.answers.monads.Reader import fpinscala.answers.monads.Reader.* import fpinscala.answers.monads.Monad.given case class Config(dbHost: String, dbPort: Int, dbName: String) // 用 ask 读取整个环境再用 map 投影出需要的字段 def dbUrl: Reader[Config, String] Reader.ask[Config].map(c sjdbc:postgresql://${c.dbHost}:${c.dbPort}/${c.dbName}) // 用 flatMap 串起两步读取环境全程不变 def connectInfo: Reader[Config, (String, String)] dbUrl.flatMap(url ReaderConfig, String.map(name (url, name))) val cfg Config(localhost, 5432, shop) println(dbUrl.run(cfg)) // jdbc:postgresql://localhost:5432/shop println(connectInfo.run(cfg)) // (jdbc:postgresql://localhost:5432/shop, shop)由于Reader已具备合法的Monad实例for推导同样可用def report: Reader[Config, String] for host - Reader.ask[Config] name - ReaderConfig, String yield s$name ${host.dbHost}每次-绑定的都是同一份Config环境——这正是与State最大的行为差异State 的for推导可以不断改写携带的状态而 Reader 的for推导只能反复读取同一个只读环境。八、延伸仓库中 Reader 思想的另一处印证「用函数读取环境」的思路在仓库中并非孤例。在 src/main/scala/fpinscala/answers/iomonad/DerivingIO.scala#L508-L518第 13 章用同样的模式定义了ConsoleReadercase class ConsoleReaderA: def mapB: ConsoleReader[B] ConsoleReader(r f(run(r))) def flatMapB: ConsoleReader[B] ConsoleReader(r f(run(r)).run(r)) object ConsoleReader: given monad: Monad[ConsoleReader] with def unitA ConsoleReader(_ a) ...它把「控制台输入字符串」当作只读环境flatMap的写法与 Reader 完全同构r f(run(r)).run(r)。从源码结构看这可以视为 Reader 思想在命令式 I/O 建模中的具体应用——读者若想进一步理解 Reader 的价值可以对照阅读这两处实现。九、小结练习 20 的官方答案虽然只有短短十几行却浓缩了 Monad 抽象与「只读环境」两个核心概念成员签名作用opaque type Reader[-R, A] R A不透明函数封装逆变环境、协变结果ask[R]Reader[R, R]读取当前环境的原语可 getrun(r)R A注入环境并执行 Readerunit(a)_ a忽略环境返回纯值flatMap(f)r f(fa(r))(r)串联计算全程复用同一环境不可 set对照仓库中的 练习版源码 完成填空再用 MonadSuite 验证四条 Monad 法则即可在依赖注入、配置传递等场景中安全地使用这一只读 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 章练习 12 详解用 flatMap(identity) 实现 Monad 的 joinflattenfpinscala 第 11 章练习 12 详解用 flatMap identity 实现 Monad 的 join flatten join 在部分资示例工程fpinscala 第 12 章练习 20用 Traverse 实现 Monad 组合composeM的类型驱动解法fpinscala 第 12 章练习 20用 Traverse 实现 Monad 组合composeM的类型驱动解法 导读 本文围绕《Functional示例工程fpinscala 第 13 章练习 3 详解实现 Free Monad 的通用解释器 runfpinscala 第 13 章练习 3 详解实现 Free Monad 的通用解释器 run 导读 本文围绕 fpinscala 仓库中 answerkey示例工程上一篇告别龟速下载3分钟掌握九大网盘直链解析神器下一篇如何在Kodi中免费实现115网盘原码播放3步简单配置终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询