
示例工程【免费下载链接】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》第 9 章解析器组合子练习 18 的官方解答answerkey/parsing/18.answer.md展开讲解如何通过给ParseError增加otherFailures: List[ParseError]字段让a | b | c这类多重分支在整体失败时不再丢失备选分支的错误信息并给出错误选择与打印策略的设计思路。读完本文你将掌握在 fpinscala 的解析器库中扩展错误模型的具体改法、or组合子的提交committed语义细节以及如何结合仓库中的Reference实现与 JSON 示例落地这一方案。问题背景or组合子失败时错误被静默丢弃fpinscala 的解析器 API 中or以及别名|用于表达先试p1失败再试p2的选择关系。在参考实现 src/main/scala/fpinscala/answers/parsing/instances/Reference.scala 中def or(p2: Parser[A]): Parser[A] l p(l) match case Failure(e, false) p2(l) case r r // committed failure or success skips running p2其中Failure(get: ParseError, isCommitted: Boolean)携带两个信息ParseError和是否已提交。未提交false的失败表示这次尝试不匹配但输入可能属于其他分支此时or会转而去运行p2已提交true的失败表示当前分支已经吃掉了输入继续尝试其他分支没有意义直接返回成功则直接返回。问题在于当p1和p2都失败时p2(l)产生的新错误会直接覆盖p1的错误——p1失败在哪个位置、期望什么这些信息全部丢失。对于a | b | c这种多分支组合用户最终只能看到最后一个分支的报错排查起来非常困难。核心方案在ParseError上扩展otherFailures字段官方解答给出的思路十分简洁既然ParseError现有的stack: List[(Location, String)]只能表达单条错误路径那就再增加一个字段专门存放其他分支产生的错误// Well just give a sketch here. The basic idea is to add an additional field to ParseError case class ParseError(stack: List[(Location,String)] Nil, otherFailures: List[ParseError] Nil): def addFailure(e: ParseError): ParseError copy(otherFailures e :: otherFailures) ...要点说明otherFailures: List[ParseError]的语义它是解析器其他分支中发生的解析错误的列表。每个元素本身就是一个完整的ParseError自带自己的stack未来还可能有嵌套的otherFailures因此可以递归地保存整棵失败树。默认值Nil与现有stack一样使用空列表作为默认值保证既有构造方式如loc.toError(msg)只填stack不受影响。练习版骨架 src/main/scala/fpinscala/exercises/parsing/Parsers.scala 中已经预先声明了这个字段case class ParseError(stack: List[(Location,String)] List(), otherFailures: List[ParseError] List()):addFailure(e)把另一个分支的错误e压到otherFailures头部e :: otherFailures。使用不可变copy保持纯函数式风格不改动原错误对象。关键联动在or的实现中填充otherFailures有了字段还要确保在or的分支切换处真正记录下被放弃分支的错误。解答给出的or实现如下extension A infix def or(p2: Parser[A]): Parser[A] s p(s) match case Failure(e, false) p2(s).mapError(_.addFailure(e)) case r r // committed failure or success skips running p2这里的核心改动只有一处当p产生未提交失败Failure(e, false)转而尝试p2时不再直接返回p2(s)的结果而是先对其调用mapError(_.addFailure(e))把p的失败e挂到p2结果的otherFailures里。这条链路依赖Result上已有的mapError辅助函数。参考实现 Reference.scala 中def mapError(f: ParseError ParseError): Result[A] this match case Failure(e, c) Failure(f(e), c) case _ this它只对Failure生效、保持提交标志c不变成功与Slice原样返回——这正是scope、label组合子改写错误的既有通道见 Reference.scala。addFailure只是复用这一通道去追加错误而不是替换。注意p2是 Parser[A]按名参数因此只有当p1未提交失败时才会被求值这与原有or的惰性分支语义保持一致。为什么已提交失败不收集因为case r r分支里r可能是成功也可能是已提交失败。已提交失败意味着当前分支已经消费了输入、语法结构已经确定此时回溯尝试其他分支本身就不该发生其错误理应直接呈现而不是作为另一个分支的失败混入otherFailures。这样otherFailures收集到的恰好都是差一点就能匹配、但被选择分支机制放弃掉的信息。错误展示与选择策略决定报告哪些错误解答在注释中特别强调引入otherFailures之后还需要配套决策如何给人看。原文指出我们必须决定如何为人类消费打印ParseError同时也可以暴露组合子用于在a | b | c链条整体失败时选择报告哪些错误——我们可能会选择收集三个解析器各自的全部错误或者只显示在输入中走得最远才失败的解析器等等。也就是说otherFailures是数据底座展示层可以自由裁剪策略一全量收集——把主错误与otherFailures递归展开把所有分支的错误逐条打印适合调试期获得最完整信息策略二最远匹配优先——比较各错误记录中Location.offset或stack最后一条的位置只展示在输入中推进最远的分支错误因为通常它最接近真正的语法错误点策略三带优先级合并——结合现有label机制把同一位置的多个消息合并成一条带分号的消息见下面介绍的collapseStack。无论选择哪种都依赖ParseError现有的展示基础设施。前一个练习练习 16的解答 answerkey/parsing/16.answer.md 以及答案包中的 Parsers.scala 给出了完整实现push(loc, msg)向stack头部压入(位置, 消息)由scope组合子调用label(s)用最新位置latestLoc重建一个只含单条消息的错误由label组合子调用latest/latestLoc取stack尾部即最早、最根本的错误作为当前最关键位置collapseStack把同一Location的多条消息按; 合并并按offset排序toString输出形如1.1 ... / 5.1 object / 5.2 key-value的行号.列号堆栈并在最底部错误处打印当前行文本与^光标指示。一个典型的展示输出来自 Parsers.scala 的注释示例1.1 file companies.json; array 5.1 object 5.2 key-value 5.10 : { MSFT ; 24, ^当otherFailures加入后toString可以按选定策略把分支错误追加到上述文本之后例如用--- other branches failed ---分隔实现主错误路径 备选分支路径的联合呈现。仓库佐证字段已预留、语义已就绪这个练习在仓库中处于待补全状态可以从多处印证设计意图练习骨架已声明字段在 src/main/scala/fpinscala/exercises/parsing/Parsers.scala 中练习版ParseError已经包含otherFailures: List[ParseError] List()说明这是官方预留的扩展点读者完成练习 18 时只需实现push、label等桩方法并接入or即可。测试解析器的同构实现测试目录中的 UnitTestParser.scala 完整实现了Result/or/flatMap/mapError其中or第 75-79 行与答案版一致——case Failure(e, false) p2(l)mapError第 31-33 行为addFailure提供了现成通道可据此验证在p2(l)外包一层mapError(_.addFailure(e))的改法无需触碰其他组合子。JSON 示例验证报错场景src/main/scala/fpinscala/answers/parsing/JSON.scala 的jsonExample用lit | obj | array等组合解析真实 JSON并演示了{ Company name ; Microsoft Corporation }冒号写成;这类畸形输入JSONSuite.scala 中的malformed JSONs属性测试则用生成器反复断言畸形输入parser.run(json).isLeft。在多分支 JSON 解析中;处的失败原本只会报lit/obj/array中最后尝试者的错误接入otherFailures后三个分支各自的失败位置如5.10 :都能被保留下来供诊断。扩展讨论从or到更深层的错误树otherFailures的引入让错误模型从一条链升级为一棵树每个ParseError的stack描述这条路径为何失败otherFailures递归地描述同一选择点还有哪些路径失败、分别败在何处。若要完全支持最远匹配优先策略可以在ParseError上增加一个辅助方法例如def furthest: (Location, String) // 在 stack 与 otherFailures 中递归寻找 offset 最大的失败点它先比较自身stack.last的位置再递归比较每个otherFailures.furthest取 offset 最大者。这样or链最深处的真实错误点就能被提取出来避免被浅层分支的错误淹没。需要留意的是性能与取舍每次分支失败都保留完整ParseError会让a | b | c这类高频选择组合子的内存与构建成本上升若对性能敏感参考实现已针对many提供循环版本以避免栈溢出见 Sliceable.scala可以只在到达展示阶段时才按需展开otherFailures或仅保留(Location, String)摘要而非完整错误对象。小结练习 18 的官方解答展示了一个教科书级的扩展思路不改动or的控制流语义只通过mapError通道给失败结果附加旁支错误。具体到本仓库在ParseError上新增otherFailures: List[ParseError]与addFailure方法练习骨架已预留字段在or的未提交失败分支中调用p2(s).mapError(_.addFailure(e))再结合已有的collapseStack/toString基础设施与furthest等选择策略决定向用户展示全部错误、最远错误还是合并后的摘要。这一改动使解析器的错误报告从只报最后一条进化为报全失败路径让a | b | c型语法的调试体验显著改善也是理解 fpinscala 解析器提交/未提交错误语义的绝佳切入点。赞分享示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载相关推荐Mediabunny 输入格式指南InputFormat、格式单例与容器识别Mediabunny 输入格式指南InputFormat、格式单例与容器识别 Mediabunny 是一套纯 TypeScript 编写的浏览器端媒体工具库示例工程如何在Interplanetary Postal Service中掌握飞船控制从新手到专家的完整教程如何在Interplanetary Postal Service中掌握飞船控制从新手到专家的完整教程 Interplanetary Postal Servic创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考