- 示例工程
【免费下载链接】fpinscala
Code, exercises, answers, and hints to go along with the book "Functional Programming in Scala"
导读
本文聚焦《Functional Programming in Scala》第 12 章(applicative)中练习 12.12 的官方提示与参考答案,深入讲解如何利用 Applicative 的map2/unit原语,将一个键值均为F效果的Map[K, F[V]]整体「翻转」为F[Map[K, V]]——即sequenceMap的实现。文章会先带你读懂提示中"把Map当作键值对列表"的关键思路,再结合本仓库 Applicative.scala 练习版 与 答案版 的完整源码,逐行拆解foldLeft折叠过程与类型推演,并顺带对比Traverse中mapTraverse的平行实现,帮助你彻底掌握 Applicative 在 Map 这类"可折叠容器"上的序列化能力。
一、练习上下文:这道题在讲什么
练习 12.12 位于第 12 章 applicative 的练习序列中,紧接练习 12.10(product/compose)与 12.11(monad 组合与 distributive law 的讨论)。它的任务是在Applicative[F]特质上新增一个方法:
def sequenceMapK, V: F[Map[K, V]]方法的语义非常直观:给定一个"普通键、效果值"的Map,把它整体翻转为"一个效果、普通 Map"——所有键保留,值从F[V]还原为V,整个过程被包进同一个F上下文。它在本质上就是sequence(把List[F[A]]变成F[List[A]])在Map容器上的推广:Map[K, F[V]]与List[(K, F[V])]存在天然的同构,因此只需要把每一对(k, fv)依次用map2累积进一个F[Map[K, V]]即可。
官方提示只有一句话,却点出了实现的核心洞察:
The standard library lets you treat a
Mapas essentially a list of pairs.
(标准库允许你把Map本质上当作一个键值对列表来对待。)
这意味着:既然 Scala 标准库的Map本身就是Iterable[(K, V)],天然支持foldLeft/foldRight等列表式折叠操作,我们完全可以直接在Map上折叠,而不必先.toList转换。下面看答案是如何顺着这条思路落地的。
二、参考答案逐行拆解:foldLeft+map2累积
12.answer.md 给出的参考实现如下:
def sequenceMapK, V: F[Map[K, V]] = ofv.foldLeft(unit(Map.empty[K, V])): case (acc, (k, fv)) => acc.map2(fv)((m, v) => m + (k -> v))逐行解读:
- 折叠起点(种子值):
unit(Map.empty[K, V])把空 Map 提升进F上下文,作为累积器的初始值。它类型为F[Map[K, V]],与最终返回类型一致。 - 折叠方向:选用
foldLeft而非foldRight。因为Map的键值对遍历顺序在语义上无关紧要(m + (k -> v)对单个键是覆盖式插入),左折叠足够且实现最简洁。 - 折叠函数:对每一对
(k, fv),调用acc.map2(fv)(...)。acc是当前累积的F[Map[K, V]],fv是当前键对应的F[V],map2把它们各自的效果层剥开、以纯函数(m, v) => m + (k -> v)组合:把新键值对插入已累积的 Map。 - 效果语义:最终得到
F[Map[K, V]],其中F的所有副作用(Option 的缺失、Either 的错误、Validated 的累积校验、List 的多重结果、State 的状态传递等)都会按各自 Applicative 实例的map2规则被正确执行与传播。
2.1 类型推演验证
假设F = Option,ofv: Map[String, Option[Int]]:
- 初始
acc0 = unit(Map.empty) = Some(Map()) - 第一轮
(k1, fv1):Some(Map()).map2(Some(1))((m, v) => m + (k1 -> v))=Some(Map(k1 -> 1)) - 第二轮
(k2, fv2):Some(Map(k1 -> 1)).map2(None)(...)=None
这正是Option语义:任何一个键对应的值缺失,整个序列化结果即为None。若换成Validated,则会按 答案版 Validated 的 map2 累积所有Invalid错误,实现"一次收集全部校验失败"的效果。
三、支撑原语:unit与map2从何而来
sequenceMap只依赖两个 Applicative 原语,它们定义在 练习版 Applicative 特质:
def unitA: F[A] extension A def map2B, C(f: (A, B) => C): F[C] = ???unit把纯值提升进效果上下文;map2把两个效果值用二元纯函数组合。
这里的关键点在于:sequenceMap是定义在Applicative特质上的派生方法,它不需要知道F具体是什么,只要F提供unit和map2,任何 Applicative(Option、Either、Validated、List、State、自定义类型等)都能自动获得对 Map 的序列化能力。这也是为什么答案中的map2走的是练习 12.2 中"由apply与unit推导出的默认实现"(见 02.answer.md)——sequenceMap站在map2之上,map2又站在apply/unit之上,构成一条自底向上的抽象阶梯。
四、对比延伸:Traverse.mapTraverse的同构实现
值得注意:sequenceMap与 Traverse 练习版 中待实现的mapTraverse高度同构。答案版 Traverse.scala 给出了它的实现:
given mapTraverse[K]: Traverse[Map[K, _]] with extension A override def traverse[G[_]: Applicative, B](f: A => G[B]): G[Map[K, B]] = m.foldLeft(summon[Applicative[G]].unit(Map.empty[K, B])): case (acc, (k, a)) => acc.map2(f(a))((m, b) => m + (k -> b))两者结构几乎一模一样:都是foldLeft+unit(Map.empty)+map2累积插入。区别在于:
sequenceMap处理的是"值已经是F[V]"的 Map,直接对F[V]做map2;mapTraverse处理的是"值还是纯值A"的 Map,先经f: A => G[B]产生G[B]再做同样的累积。
若定义f = identity(A = F[V]情形),traverse便退化为sequence,与sequenceMap殊途同归。这也印证了提示中"Map 本质上是键值对列表"的普适性:任何能用foldLeft折叠的容器,都可以用同样的unit+map2配方实现序列化。
五、实操验证:在仓库中运行与自测
本仓库为每个练习都提供了脚手架、答案与测试骨架,你可以这样验证sequenceMap:
打开练习文件:在 练习版 Applicative.scala 中找到
sequenceMap的???占位,填入上面的实现(建议先独立尝试,卡住再看 12.answer.md)。对照答案:完整实现位于 答案版 Applicative.scala,可作为类型与语义的参照。
编译与交互验证:在仓库根目录执行
scala-cli compile .编译全部练习与答案;需要 REPL 试跑时执行scala-cli console .,例如:scala> import fpinscala.answers.applicative.Applicative.{optionMonad, given} scala> val ofv = Map("a" -> Some(1), "b" -> Some(2)) scala> import fpinscala.answers.applicative.Applicative.{*, given} scala> ofv.foldLeft(...) // 或用答案中的 sequenceMap 直接验证更简单的做法是直接构造
Map[String, Option[Int]]后调用答案版Applicative[Option]的sequenceMap,观察"某个值为 None 时整体返回 None"的效果传播。运行测试:执行
scala-cli test .可运行全部单元测试(注意:未完成的练习对应测试会失败,这是 README 明确说明的预期行为,README.md)。目前src/test下尚无针对sequenceMap的专门测试用例,但你可以参照 src/test/scala/fpinscala/exercises 下既有套件的写法自行补充。
提示:本项目基于 Scala CLI 构建(另提供 SBT 构建,见 project/plugins.sbt 与 README.md),执行编译、REPL 与测试的完整命令清单都在 README.md 中。
六、小结:从一条提示到一类模式
练习 12.12 的提示虽短,却浓缩了函数式编程中一个高频模式:把可折叠的容器当作"对列表"来处理,用unit造种子、用map2累积、用折叠遍历。掌握sequenceMap后,你可以把同样的配方迁移到任意Foldable容器(List、Tree、Map、Option 乃至自定义数据类型),这也正是下一节练习 12.13Traverse与mapTraverse将要系统化的内容——届时你会发现,sequenceMap只是"对所有可遍历结构统一抽象"的一个具体实例。
- 示例工程
【免费下载链接】fpinscala
Code, exercises, answers, and hints to go along with the book "Functional Programming in Scala"
相关推荐
fpinscala 第 12 章实战:用 Applicative 的 `sequenceMap` 实现 `Map[K, F[V]] → F[Map[K, V]]`
fpinscala 第 12 章实战:用 Applicative 的 sequenceMap 实现 Map K, F V → F Map K, V 导读 本文围
示例工程fpinscala 精讲:用 Applicative 的 traverse/sequence 实现列表转置(第 12 章练习 04)
fpinscala 精讲:用 Applicative 的 traverse/sequence 实现列表转置(第 12 章练习 04) 导读 本篇文章围绕 fpi
示例工程fpinscala 练习 12.8 详解:用 `Applicative.product` 组合两个 Applicative 实例
fpinscala 练习 12.8 详解:用 Applicative.product 组合两个 Applicative 实例 本篇技术指南聚焦《Function
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考