用内函数幺半群(Endofunction Monoid)实现 foldRight 与 foldLeft:FP in Scala 第 10 章练习 6 深度解析

发布时间:2026/10/13 6:55:03

用内函数幺半群(Endofunction Monoid)实现 foldRight 与 foldLeft:FP in Scala 第 10 章练习 6 深度解析 示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载导读本文围绕 fpinscala 仓库中 answerkey/monoids/06.hint.md 这一练习提示展开深入拆解《Functional Programming in Scala》第 10 章monoids练习 6 的核心思想把传入foldRight的二元函数(A, B) B柯里化为A (B B)从而借用内函数幺半群endofunction monoid即B B在函数复合下构成的幺半群把任意列表折叠统一收纳进foldMap。读完本文你将掌握从类型签名出发推导实现的完整思路、foldLeft作为对偶dual的构造方式以及compose/andThen方向约定对最终代码形态的决定性影响并能在仓库源码与测试用例中验证结论。练习背景Monoid、foldMap 与前置练习链在进入练习 6 之前仓库中的练习序列已经铺垫了三块基石理解它们才能读懂 hint 的提示练习 2——dual对偶幺半群任何一个幺半群都可以通过翻转combine的入参顺序得到对偶幺半群且empty不变。参考答案见 answerkey/monoids/02.answer.mddef dualA: Monoid[A] new: def combine(x: A, y: A) m.combine(y, x) val empty m.empty练习 3——endoMonoid内函数幺半群对任意类型AA A在函数复合下构成幺半群identity是单位元。参考答案见 answerkey/monoids/03.answer.md。练习 5——foldMap把map和fold合并为一次遍历通过幺半群完成归约。参考答案见 answerkey/monoids/05.answer.mddef foldMapA, B(f: A B): B as.foldLeft(m.empty)((b, a) m.combine(b, f(a)))练习 6 的任务正是只用foldMap以及前面定义好的endoMonoid、dual重新实现foldRight与foldLeft。对应的练习占位代码位于 src/main/scala/fpinscala/exercises/monoids/Monoid.scala目前以???形式等待填充def foldMapA, B(f: A B): B ??? def foldRightA, B(acc: B)(f: (A, B) B): B ??? def foldLeftA, B(acc: B)(f: (B, A) B): B ???核心洞察把(A, B) B柯里化为A (B B)hint 的第一句话是整个练习的灵魂Notice that the type of the function that is passed tofoldRightis(A, B) B, which can be curried toA (B B).foldRight的折叠函数类型是(A, B) B——它同时接受列表元素和累加器。对它做柯里化currying后得到f.curried: A (B B)这个类型签名极具暗示性对每个元素a: Af.curried(a)就是一个B B的内函数。而恰好在练习 3 中我们已经知道B B对任意B都构成幺半群endoMonoid。于是思路立刻清晰起来用foldMap把列表as中的每个元素a映射成内函数f.curried(a)用endoMonoid把这些内函数复合成一个总函数最后把这个总函数作用到初始累加器acc上。整个过程把折叠这个动作彻底抽象成了复合函数这个纯代数概念——这正是幺半群抽象能力的体现foldMap 一次遍历即可完成映射与归约。为什么B B是一个幺半群要确认上述方案可行需要验证B B确实满足幺半群的两条定律单位元identityidentity: B B即x x。f compose identity f、identity compose f f对任意f成立。结合律associativity函数复合天然满足结合律(f compose g) compose h f compose (g compose h)。需要注意的是compose与andThen只是同一件事的两种书写方向f andThen g g compose f仓库的完整实现 src/main/scala/fpinscala/answers/monoids/Monoid.scala 选择了andThen方向并注释说明这里存在方向选择// There is a choice of implementation here as well. // Do we implement it as f compose g or f andThen g? We have to pick one. def endoMonoid[A]: Monoid[A A] new: def combine(f: A A, g: A A) f andThen g val empty identity方向约定compose还是andThen会直接影响后续foldRight/foldLeft使用endoMonoid还是dual(endoMonoid)这是本文后续要重点辨析的细节。用 foldMap endoMonoid 实现 foldRight先做类型层面的推演。设endoMonoid[B]: Monoid[B B]则foldMap(as, endoMonoid[B])(f.curried): B B再作用到acc上得到B恰好就是foldRight的返回类型。参考答案answerkey/monoids/06.answer.md给出的核心形式是def foldRightA, B(acc: B)(f: (A, B) B): B foldMap(as, endoMonoid)(f.curried)(acc)下面用具体例子验证折叠顺序。取as List(1, 2)、acc 0、f (a, b) a.toString b标准foldRight的结果应为120即f(1, f(2, 0))。f.curried(1) b 1 bf.curried(2) b 2 b若endoMonoid以f compose g定义combine(f, g) f compose gfoldMap从左向右累积复合得到(b 1 b) compose (b 2 b)作用到0上得到1 (2 0) 120正确若endoMonoid以f andThen g定义foldMap累积出的总函数作用到0上会得到210顺序颠倒——此时就必须改用dual(endoMonoid)。这正对应仓库完整实现 src/main/scala/fpinscala/answers/monoids/Monoid.scala 中的写法与注释// The function type (A, B) B, when curried, is A (B B). // And B B is a monoid for any B (via function composition). // We have to flip the order of params to the endo monoid since we // chose to define it as f andThen g instead of f compose g. def foldRightA, B(acc: B)(f: (A, B) B): B foldMap(as, dual(endoMonoid))(f.curried)(acc)关于方向的两种约定务必辨析由于endoMonoid本身是练习 3 的待实现函数方向由实现者决定因此存在两组等价但形态相反的写法endoMonoid的定义方向foldRight的写法foldLeft的写法combine(f, g) f compose ghint 字面建议foldMap(as, endoMonoid)(f.curried)(acc)foldMap(as, dual(endoMonoid))(a b f(b, a))(acc)combine(f, g) f andThen g本仓库实现foldMap(as, dual(endoMonoid))(f.curried)(acc)foldMap(as, endoMonoid)(a b f(b, a))(acc)hint 的原话use the endofunction monoidB Bto implementfoldRight。The implementation offoldLeftis then just the dual对应的是第一行compose方向。本仓库在练习 3 解答与完整实现中选择andThen方向因此在第二行——方向翻转通过dual来补偿二者殊途同归。写作时请务必先确定自己endoMonoid的方向再决定是否套用dual否则折叠顺序会颠倒。foldLeft 是 foldRight 的对偶翻转参数 翻转幺半群hint 的第二句话点明foldLeft的构造法The implementation offoldLeftis then just the dual.foldLeft的折叠函数类型是(B, A) B——累加器在左、元素在右与foldRight恰好相反。因此需要做两处翻转翻转函数参数把f: (B, A) B改写成a b f(b, a)得到A (B B)翻转幺半群方向因为foldLeft从左往右累积而foldMap内部也是从左往右用combine累积需要用dual把组合方向倒转使内函数复合后作用顺序符合左折叠语义。参考答案answerkey/monoids/06.answer.md在compose约定下的写法// Folding to the left is the same except we flip the arguments to // the function f to put the B on the correct side. // Then we have to also flip the monoid so that it operates from left to right. def foldLeftA, B(acc: B)(f: (B, A) B): B foldMap(as, dual(endoMonoid))(a b f(b, a))(acc)本仓库在andThen约定下的完整实现src/main/scala/fpinscala/answers/monoids/Monoid.scala则是不加dual的版本def foldLeftA, B(acc: B)(f: (B, A) B): B foldMap(as, endoMonoid)(a b f(b, a))(acc)用非对称函数验证顺序取as List(1, 2)、acc 0、f (b, a) a.toString b注意这里是foldLeft的签名(B, A) B标准foldLeft结果为210即f(f(0, 1), 2)。foldMap累积出的总函数作用到0上恰好得到210说明顺序正确。补充一点foldMap本身在 Monoid.scala 中是用标准库List.foldLeft实现的因此用foldMap实现foldLeft并不构成循环依赖——foldMap是地基我们自定义的foldRight/foldLeft都建在它之上。效率提示为什么不要担心不够高效hint 最后特意安抚读者Dont worry if these implementations are not very efficient.从实现结构看这种写法确实比直接折叠低效原因可以归纳为三点属于对代码结构的推断而非性能基准数据为每个元素构造一个闭包f.curried(a)对列表的每个元素都会生成一个新的B B函数对象多一层函数复合foldMap用combine逐步复合这些函数最终得到的是一个函数的函数最终一次调用才真正折叠所有计算都推迟到把总函数作用到acc的那一刻才发生中间经历了整条函数调用链。因此这种实现的主要价值在于展示幺半群与foldMap的抽象威力、加深对折叠 复合 应用的理解而非追求性能。工程实践中直接用标准库或尾递归实现折叠更合适。仓库源码answers 完整实现与 Foldable 中的复用完整答案位于 src/main/scala/fpinscala/answers/monoids/Monoid.scala四个核心函数一气呵成def foldMapA, B(f: A B): B as.foldLeft(m.empty)((b, a) m.combine(b, f(a))) def foldRightA, B(acc: B)(f: (A, B) B): B foldMap(as, dual(endoMonoid))(f.curried)(acc) def foldLeftA, B(acc: B)(f: (B, A) B): B foldMap(as, endoMonoid)(a b f(b, a))(acc)更值得注意的是这套内函数幺半群 对偶的技巧在仓库中被提升到了抽象类型层面src/main/scala/fpinscala/answers/monoids/Foldable.scala 的Foldable[F[_]]trait 以扩展方法的形式为任意可折叠容器统一提供foldRight、foldLeft、foldMap、combineAlltrait Foldable[F[_]]: import Monoid.{endoMonoid, dual} extension A def foldRightB(f: (A, B) B): B as.foldMap(f.curried)(using dual(endoMonoid[B]))(acc) def foldLeftB(f: (B, A) B): B as.foldMap(a b f(b, a))(using endoMonoid[B])(acc) def foldMapB(using mb: Monoid[B]): B as.foldRight(mb.empty)((a, b) mb.combine(f(a), b))这里foldMap与foldRight互为表里Foldable用foldRight定义foldMap而Monoid对象用foldMap定义foldRight——两者在List、IndexedSeq、LazyList、Tree、Option等具体given实例中通过覆写各取所长例如Foldable[IndexedSeq]的foldMap直接委托给平衡递归的Monoid.foldMapV。这正是练习 6 的思想被抽象化后的形态只要一个类型拥有幺半群化的折叠能力foldRight/foldLeft就能免费获得。用测试验证实现仓库为 monoids 章节准备了完整的属性测试。练习 6 的相关断言在 src/test/scala/fpinscala/exercises/monoids/MonoidSuite.scala 中test(Monoid.foldMap)(genBooleanList): list assertEquals(foldMap(list, intAddition)(trueCounter), trueCounter(list)) test(Monoid.foldRight)(genBooleanList): list assertEquals(foldRight(list)(0)((b, acc) trueCounter(b) acc), trueCounter(list)) test(Monoid.foldLeft)(genBooleanList): list assertEquals(foldLeft(list)(0)((acc, b) trueCounter(b) acc), trueCounter(list))其中trueCounter(list) list.count(identity)即统计布尔列表中true的个数。测试以随机生成的布尔列表为输入把foldRight/foldLeft的结果与直接计数比对。由于加法交换律的作用该测试对左右折叠顺序不敏感——所以即便方向搞反也可能通过这正是建议读者自行用非对称函数如字符串拼接或减法做交叉验证的原因。此外FoldableSuite.scala 还会对List、IndexedSeq、LazyList、Tree、Option五种容器的foldRight/foldLeft/foldMap/combineAll做一致性校验可作为抽象层实现正确性的佐证。按仓库 README.md 的说明可以用 Scala CLI 运行 monoids 包的测试scala-cli test . -- fpinscala.exercises.monoids.*或者进入 REPL 手工实验scala-cli console .需要提醒的是练习文件中的foldMap、foldRight、foldLeft目前都是???占位直接运行测试会失败按本文方案填充实现后相应测试即会转绿——这也与 README 中随着你解出练习测试开始通过的说明一致。小结练习 6 的核心价值不在于用另一种方式写折叠而在于一次类型驱动的抽象跃迁观察类型(A, B) B柯里化后是A (B B)暴露出每个元素对应一个内函数的结构复用幺半群B B在函数复合下天然是幺半群identity为单位元复合满足结合律因此foldMapendoMonoid即可完成折叠对偶补全foldLeft只需翻转函数参数顺序并用dual翻转幺半群方向方向辨析compose与andThen两种约定导致endoMonoid/dual(endoMonoid)的选用互换本仓库源码以andThen方向给出了自洽且带注释的实现效率取舍该写法以函数对象与复合为代价换取概念上的统一适合教学演示而非性能敏感场景。配合 answerkey/monoids/06.hint.md 的提示、answerkey/monoids/06.answer.md 的参考答案、answers 完整实现 与 测试套件读者可以从提示 → 推导 → 实现 → 验证四个环节完整消化这一经典练习并顺带理解Foldable抽象层的设计动机。赞分享示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载相关推荐《Scala 函数式编程》Monoid 练习精解从零定义四个基础幺半群实例《Scala 函数式编程》Monoid 练习精解从零定义四个基础幺半群实例 导读 本文以 fpinscala 仓库第 10 章monoids的第一道练习解示例工程用 Monoid 与 foldMapV 实现单词计数fpinscala 第 10 章练习 11 源码详解用 Monoid 与 foldMapV 实现单词计数fpinscala 第 10 章练习 11 源码详解 导读 本文围绕 fpinscala 仓库 answe示例工程fpinscala 第 6 章 Applicative 练习 6用 Monoid 为 Validated 实现错误累积的 Applicative 实例fpinscala 第 6 章 Applicative 练习 6用 Monoid 为 Validated 实现错误累积的 Applicative 实例 本文围示例工程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/10/13 6:55:03

第1章:开发环境搭建,在 Windows 中安装 VS 2019

专栏导航 上一篇:第1章:下载 Linux 0.12 内核 回到目录 下一篇:第2章:Linux 基础,date 命令 本节前言 对于本节所讲解的知识,有可能,你会需要时不时地参考本专栏的其它文章。真的遇到了需要…

2026/10/13 6:55:03

天地盖 vs 普通开槽摇盖纸箱:承重性能与装箱效率核心区别

天地盖 vs 普通开槽摇盖纸箱:承重性能与装箱效率核心区别一、对比引言天地盖纸箱与普通开槽摇盖纸箱(RSC 型)是工业包装中最主流的两类箱型,二者结构形式完全不同,在承重性能、装箱效率、自动化适配、成本等维度差异显…

2026/10/13 8:00:06

Open Code Review:一种以透明度为基础设施的异步协作范式

1. 项目概述:这不是代码检查,而是一场协作范式的重构“open-code-review”这个词组乍看像一个技术名词,但其实它根本不是某个工具、平台或协议的官方命名——它是一类实践行为的精准描述,是开发者群体在真实协作中自发形成的一套共…

2026/10/13 8:00:06

AgentScope核心解析:Actor模型与多智能体协作框架详解

AgentScope 这个名字,最近在关注 Agent 应用开发的人应该都听过。但说实话,我第一次接触它的时候挺懵的:网上资料东一榔头西一棒子,有人说它是编排框架,有人说它是分布式调度工具,还有人提它跟低代码平台有…

2026/10/13 8:00:06

MIT 6.824 Lab1:用Go实现容错MapReduce调度器

1. 这不是“写个MapReduce”,而是MIT分布式系统课的第一次真实心跳 刚接触6.824这门课的同学,看到“lab1”三个字,容易下意识以为是“照着文档敲几行代码就能跑通的入门练习”。我当年也是这么想的——直到在凌晨三点盯着屏幕里反复崩溃的wor…

2026/10/13 8:00:06

LiteLLM:统一多模型API网关的工程实践与落地指南

1. 项目概述:LiteLLM 是什么,它解决的到底是什么问题? LiteLLM 这个名字乍一听像某个轻量级模型,但其实它压根不训练模型、不推理模型、也不压缩模型——它是个“协议翻译器”,更准确地说,是一个 统一 API…

2026/10/13 8:00:06

五款GNSS数据处理软件基线解算对比:试验设计与差异归因

简介:《五款GNSS数据处理软件基线解算结果研究》是一份面向卫星导航数据处理、测绘工程与定位应用人员的专业技术文献。研究基于某工程控制网六条实测基线,系统对比CGO、TBC、SOUTH、国产CGO及国外LGO等五款软件在不同采样间隔(5至60秒&#…

2026/10/13 7:55:06

PyTorch迁移学习实战:蚂蚁蜜蜂二分类入门指南

1. 项目概述:为什么蚂蚁蜜蜂分类是迁移学习的“黄金入门题”你打开PyTorch官方教程,翻到迁移学习章节,第一眼看到的几乎总是那个经典案例:用ResNet在ImageNet预训练权重上微调,完成蚂蚁(ant)和蜜…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/12 12:01:26

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介:这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师,聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案,解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件,约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型,最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支,让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图,十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑