尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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),仅供参考
RELATED

相关推荐

YOLO垃圾四分类数据集制作全指南:从标注到验收的工程实践

YOLO垃圾四分类数据集制作全指南:从标注到验收的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/12 2:07:32
XQuad 编译指南:读懂 `problem.compile()` 生成的 encoder / verifier / decoder 三份 XQASM

XQuad 编译指南:读懂 `problem.compile()` 生成的 encoder / verifier / decoder 三份 XQASM

【免费下载链接】xquad A rust implementation of the Quip Networks quantum virtual machine. 项目地址: https://gitcode.com/gh_mirrors/xq/xquad 点击查看 免费下载 problem.compile() 是 XQuad 约束编程层(xqcp)的核心出口&#xff1a…

📅 2026/10/12 2:02:31
Psalm 的 MissingClosureReturnType 全面解析:从触发机制、错误级别到自动修复

Psalm 的 MissingClosureReturnType 全面解析:从触发机制、错误级别到自动修复

开发工具代码质量质量保障 【免费下载链接】psalm A PHP static analysis tool for finding errors and security vulnerabilities in PHP applications 项目地址: https://gitcode.com/gh_mirrors/ps/psalm 点击查看 免费下载 本篇技术指南以 Psalm 静态分析工具中…

📅 2026/10/12 2:02:31
MORE NEWS

更多资讯

📰

UC网盘下载不限速办法:亲测有效的免费提速思路与操作指南

UC网盘下载不限速的办法,亲测有效的免费加速思路都在这了用UC浏览器的人几乎人手一个UC网盘,平时存点资料、传个文件确实方便,但真到下载大文件的时候,那进度条走得叫一个折磨。明明家里宽带是五百兆,眼见着其他App下载…

📰

Flutter跨平台开发鸿蒙旅行攻略App全流程实践与适配避坑指南

前段时间我把一个旅行攻略规划App从零到一完整跑了一遍,目标平台锁在鸿蒙系统,技术栈选了Flutter框架做跨平台开发。整个过程比想象中曲折,特别是鸿蒙原生能力接入这一块,很多问题在网上找不到现成答案,只能自己反复摸…

📰

广东省SHP数据处理指南:行政区划与路网分析实战

简介:一份基于Shapefile(SHP)格式的广东省地理空间数据包,面向GIS开发、城市规划与交通研究人群,提供省、市、县三级行政区划边界,以及道路网、铁路网两类关键交通要素。借助这些矢量图层,可直接…

📰

钉钉集成API Java实战:Token缓存与高频消息推送避坑指南

简介:面向Java开发者的钉钉集成参考资料,系统演示如何将企业业务系统与阿里钉钉开放平台对接,涵盖OAuth授权、消息推送、通讯录管理以及审批流自动化等核心场景,适合承担企业内部集成开发任务的中级及以上工程师。压缩包共174个文…

📰

树莓派Pico C/C++开发环境搭建:CLion+SDK+OpenOCD调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Linux下查看线程的实用方法:从ps、top到pidstat全掌握

排查 Linux 线程问题的时候,最怕的不是不会用命令,而是用错了命令看错了对象。很多刚接触 Linux 的同学,一上来就问“怎么查看线程”,但真正到了现场,往往连线程和进程的关系都还没理清楚——拿ps一顿输出,…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬