fpinscala 单调性检查:用 Monoid 与 foldMapV 实现 O(log n) 有序序列判定

发布时间:2026/10/12 16:05:16
fpinscala 单调性检查:用 Monoid 与 foldMapV 实现 O(log n) 有序序列判定
示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载本章主题源自《Functional Programming in Scala》fpinscala 仓库第 10 章Monoids的练习 9。原文文档 answerkey/monoids/09.answer.md 给出了一组精炼但完整的答案代码借助区间 无序标记这一状态载体把判断一个IndexedSeq[Int]是否非降序有序这个看似不可并行化的问题转化为一个可以并行折叠的 Monoid 实例再交由foldMapV以分治方式完成判定。读完本文你将理解区间折叠这一 Monoid 设计范式能够独立实现ordered判定函数并掌握如何把它嵌入仓库的练习测试体系进行验证。练习背景从 foldMapV 到并行折叠练习 9 所在的章节围绕 Monoid 的分治折叠展开。在 src/main/scala/fpinscala/answers/monoids/Monoid.scala 中foldMapV被定义为对IndexedSeq的平衡递归折叠def foldMapVA, B(f: A B): B if as.length 0 then m.empty else if as.length 1 then f(as(0)) else val (l, r) as.splitAt(as.length / 2) m.combine(foldMapV(l, m)(f), foldMapV(r, m)(f))foldMapV把序列从中间一分为二递归地对两半分别做foldMap再用 Monoid 的combine合并结果。它的作用域不止于本练习——在同一个文件里parFoldMap正是借助foldMapV结合ParMonoid 实现并行归约对应练习 8见 answerkey/monoids/08.answer.mdcount单词计数练习 10、11也复用了它。因此只要为有序性判断构造出合适的 Monoid同样的问题就能以任意顺序、任意分区方式合并求解。核心思路区间 无序标记的 Monoid练习 9 的难点在于判断整个序列有序不能简单地把左半、右半各自是否有序用布尔值合并——因为即使左右两半各自有序连接处仍可能无序左半的最后一个元素大于右半的第一个元素。答案是提示文档 answerkey/monoids/09.hint.md 所说的让每个片段携带两类信息区间interval该片段中元素的最小值与最大值无序标记该片段内部是否已经发现违反有序性的位置。只要左右两半都内部有序且左半最大值leftMax不超过右半最小值rightMin合并后的片段就依然有序否则无序标记置为false。合并后的区间自然是(leftMin, rightMax)。参考答案逐行拆解练习 9 的完整答案如下摘自 answerkey/monoids/09.answer.mdopaque type Interval (Int, Int) val orderedMonoid: Monoid[(Boolean, Option[Interval])] new: def combine(a1: (Boolean, Option[Interval]), a2: (Boolean, Option[Interval])) (a1(1), a2(1)) match case (Some((leftMin, leftMax)), Some((rightMin, rightMax))) (a1(0) a2(0) leftMax rightMin, Some((leftMin, rightMax))) case _ (a1(0) a2(0), a1(1).orElse(a2(1))) val empty (true, None) def ordered(ints: IndexedSeq[Int]): Boolean foldMapV(ints, orderedMonoid)(i (true, Some((i, i))))(0)逐部分解析1. 不透明类型别名Intervalopaque type Interval (Int, Int)opaque type不透明类型别名是 Scala 3 的机制在定义它的作用域内Interval可以被当作(Int, Int)使用在作用域之外Interval与元组类型互不兼容防止把任意二元组误当作区间传入。练习答案直接写成(Int, Int)亦可opaque的价值在于类型安全与可读性。2. Monoid 载体(Boolean, Option[Interval])Monoid 的元素类型是一个二元组第一个分量Boolean记录到目前为止该片段内部是否有序第二个分量Option[Interval]记录该片段的取值范围。用Option包装是因为空片段没有取值区间需要以None表示Monoid的empty必须能代表空片段。3.combine的两种分支combine对两个片段的区间做模式匹配两个片段都有区间Some/Some分支若a1内部有序、a2内部有序且leftMax rightMin左半最大值不超过右半最小值则合并结果有序新区间为(leftMin, rightMax)至少一个片段为空case _分支合并后是否有序取决于两者原有标记的与运算a1(0) a2(0)区间用orElse取非空者——这正是 src/main/scala/fpinscala/answers/monoids/Monoid.scala 中optionMonoid的combine(x, y) x orElse y语义空片段对合并结果不产生任何影响。4.empty (true, None)空序列被判定为有序true且没有取值区间None。这是恒等元任意片段与空片段合并结果保持原片段不变——(true, None)与(b, Some(iv))合并走case _分支得到(true b, None.orElse(Some(iv))) (b, Some(iv))。5. 映射函数i (true, Some((i, i)))单个元素映射为有序、区间即该元素本身——这正是区间模型的边界情形元素本身就是片段的最小值与最大值。6. 末尾的(0)元组的投影答案代码末尾的(0)是元组的第一分量投影等价于._1。因此ordered的返回类型是Boolean而非整个二元组foldMapV(ints, orderedMonoid)(i (true, Some((i, i))))(0)对任意ints应用后得到整个序列是否有序的布尔值。验证区间合并的正确性判定leftMax rightMin就能保证连接处有序其依据是若左右两半各自内部有序那么左半的最大值必然是其最后一个元素右半的最小值必然是其第一个元素只要左半最后一个元素 ≤ 右半第一个元素整个序列在连接处就不会出现逆序。区间合并因此是自洽的合并后的区间(leftMin, rightMax)涵盖了左右两半的全部取值。仓库提供的完整实现src/main/scala/fpinscala/answers/monoids/Monoid.scala 第 127140 行采用了等价但更结构化的写法用case class Interval(ordered: Boolean, min: Int, max: Int)直接承载有序标记 区间把三元状态合并进一个类型combine的分支处理与答案完全一致case class Interval(ordered: Boolean, min: Int, max: Int) val orderedMonoid: Monoid[Option[Interval]] new: def combine(oa1: Option[Interval], oa2: Option[Interval]) (oa1, oa2) match case (Some(a1), Some(a2)) Some(Interval(a1.ordered a2.ordered a1.max a2.min, a1.min, a2.max)) case (x, None) x case (None, x) x val empty None def ordered(ints: IndexedSeq[Int]): Boolean foldMapV(ints, orderedMonoid)(i Some(Interval(true, i, i))) .map(_.ordered).getOrElse(true)两种写法在语义上完全等价答案用(Boolean, Option[Interval])二元组区分有序标记与区间完整实现把它们合并进Interval再整体包一层Optionempty分别是(true, None)与None映射函数分别产生(true, Some((i, i)))与Some(Interval(true, i, i))。从空序列有序这一约定可以看出单元素与空序列都是有序的ordered(IndexedSeq.empty)与ordered(IndexedSeq(42))均返回true。测试验证与运行方式仓库为这道练习提供了属性测试。在 src/test/scala/fpinscala/exercises/monoids/MonoidSuite.scala 中Monoid.ordered的测试把答案实现与被测实现的结果直接对照test(Monoid.ordered)(genIntIndexedSeq): ints assertEquals(ordered(ints), ints ints.sorted)即ordered(ints)等价于ints与其排序结果相等。这里的ordered来自import fpinscala.exercises.monoids.Monoid.*测试借助仓库的PropSuite对genIntIndexedSeq生成的随机整数序列断言该性质。更严格的语义校验是直接用foldMapV的平衡合并结果与线性扫描结果比对由于combine满足结合律任意合并顺序得到的最终布尔值都相同。仓库使用 Scala CLI 构建参见 README.md。在仓库根目录下运行全部测试scala-cli test .单跑 monoids 包的相关测试scala-cli test . -- fpinscala.exercises.monoids.*设计要点与延伸思考Monoid 的关键是合并即局部判定本练习的精髓在于有序性判定被编码进combine的局部比较leftMax rightMin中而不依赖全局遍历顺序。这正是 Monoid 支持并行与分治折叠的根本原因——只要局部合并规则自洽合并顺序就无关紧要。Option用于表达无区间空片段没有 min/maxempty (true, None)让恒等元同时满足空序列有序与合并不影响结果两个要求与optionMonoid的orElse语义一脉相承。从foldMapV到parFoldMap理解了本练习的foldMapV(ints, orderedMonoid)(...)调用形态就能自然迁移到练习 8 的parFoldMap——只需把 Monoid 提升到Par上下文中par(m)foldMapV的递归分支即可并行执行有序性判定随之获得并行化能力。opaque type的类型卫生练习答案用opaque type Interval (Int, Int)把区间从裸元组中抽象出来在获得可读性的同时避免了外部代码把任意(Int, Int)当作区间使用。若想继续深入 Monoid 的同类技巧可阅读同章后续练习answerkey/monoids/10.answer.mdWC单词计数 Monoid与 answerkey/monoids/11.answer.md借助foldMapV的count它们与本文共享同一套局部状态 合并规则的建模范式。赞分享示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载相关推荐用 Monoid 与 foldMapV 实现单词计数fpinscala 第 10 章练习 11 源码详解用 Monoid 与 foldMapV 实现单词计数fpinscala 第 10 章练习 11 源码详解 导读 本文围绕 fpinscala 仓库 answe示例工程xi-editor Rope Science四用单调 Monoid 在 Rope 上实现 O(log n) 括号匹配xi editor Rope Science四用单调 Monoid 在 Rope 上实现 O log n 括号匹配 本文为 xi editor READ开发工具codeforces-go 题解精讲Minimum Array Length After Pair Removals——有序数组众数判定与 O(log n) 二分查找codeforces go 题解精讲Minimum Array Length After Pair Removals——有序数组众数判定与 O log n 二科学计算创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考