news 2026/10/12 3:25:24

fpinscala 练习 12.12 精解:用 Applicative 的 `sequenceMap` 反转 Map 的效果层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fpinscala 练习 12.12 精解:用 Applicative 的 `sequenceMap` 反转 Map 的效果层
  • 示例工程

【免费下载链接】fpinscala

Code, 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》第 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 aMapas 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))

逐行解读:

  1. 折叠起点(种子值):unit(Map.empty[K, V])把空 Map 提升进F上下文,作为累积器的初始值。它类型为F[Map[K, V]],与最终返回类型一致。
  2. 折叠方向:选用foldLeft而非foldRight。因为Map的键值对遍历顺序在语义上无关紧要(m + (k -> v)对单个键是覆盖式插入),左折叠足够且实现最简洁。
  3. 折叠函数:对每一对(k, fv),调用acc.map2(fv)(...)。acc是当前累积的F[Map[K, V]],fv是当前键对应的F[V],map2把它们各自的效果层剥开、以纯函数(m, v) => m + (k -> v)组合:把新键值对插入已累积的 Map。
  4. 效果语义:最终得到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:

  1. 打开练习文件:在 练习版 Applicative.scala 中找到sequenceMap的???占位,填入上面的实现(建议先独立尝试,卡住再看 12.answer.md)。

  2. 对照答案:完整实现位于 答案版 Applicative.scala,可作为类型与语义的参照。

  3. 编译与交互验证:在仓库根目录执行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"的效果传播。

  4. 运行测试:执行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"

项目地址:https://gitcode.com/gh_mirrors/fp/fpinscala
点击查看免费下载
上一篇:N_m3u8DL-RE 手把手流媒体下载指南
下一篇:LrcHelper:网易云音乐双语歌词下载完整指南 - 轻松获取精准歌词

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 3:23:01

虚拟电厂日前日内双时间尺度调度:Matlab+Yalmip建模详解

这两年做虚拟电厂(VPP)优化调度,我前前后后复现过不少公开论文里的模型,最实用、落地价值最高的仍然是“日前调度 日内调度”这套双时间尺度框架。网上流传的版本很多,但真正把两层逻辑讲清楚、代码能直接跑通的却不多…

作者头像 李华
网站建设 2026/10/12 3:22:52

remocn一次性讲透27种Remotion场景转场:从硬切到Shader擦除

【免费下载链接】remocn Production-ready animations, transitions, backgrounds, and scenes for Remotion 项目地址: https://gitcode.com/gh_mirrors/re/remocn 点击查看 免费下载 remocn 是一个面向 Remotion 的复制粘贴式动效组件库,它的 Transit…

作者头像 李华
网站建设 2026/10/12 3:20:46

mysql获取分组中的指定数据(附四大排序函数说明)

目录一.背景二.解决方案1.先排序后分组方式2.利用rank() over...(推荐)3 mysql四大排名函数(1)排序条件下的排名(2)分区排序条件下的排名一.背景 : 举个例子,现有两张表分别是老师和…

作者头像 李华
网站建设 2026/10/12 3:20:42

CubeFS 中的 HttpRouter 深入解析:Go 高性能 HTTP 路由器的原理与实战

存储分布式文件系统对象存储云原生 【免费下载链接】cubefs cloud-native distributed storage 项目地址: https://gitcode.com/gh_mirrors/cu/cubefs 点击查看 免费下载 HttpRouter 是一个基于压缩字典树(Radix Tree)实现的高性能 Go HTTP …

作者头像 李华
网站建设 2026/10/12 3:20:31

无线路由器当无线AP用:网线插LAN口,手把手配置指南

/* 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 3:20:22

STM32寄存器白话手册:从硬件信号流理解GPIO、时钟与中断

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

作者头像 李华