
示例工程【免费下载链接】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第 8 章“属性测试Property-Based Testing”中的练习 16 展开逐行拆解其标准答案gpy2——一个能够生成“结构随输入长度变化”的Gen[Par[Int]]并行计算生成器并顺带讲解练习 17 中基于它写出的forkPropPar.fork(y) y的 fork 律属性。读完本文你将掌握Gen组合子choose、listOfN、map、foldLeft在并行程序测试中的实际用法理解forAllPar这类“执行器 被测值”联合生成的测试基础设施以及仓库答案模块中“穷举 随机”双重生成机制的底层原理。一、练习定位为什么需要“结构有变化”的并行计算第 8 章的目标是自建一个极简的属性测试库用Gen[A]生成随机输入用Prop描述性质再通过大量采样来寻找反例。前几道练习依次实现了Gen的基础组合子choose、unit、listOfN、flatMap、union、weighted与Prop的组合逻辑、||、tag、forAll。到了练习 16测试对象换成了第 7 章开发的并行库Par。最简单的并行程序生成器是练习 15 之前的gpyval gpy: Gen[Par[Int]] Gen.choose(0, 10).map(Par.unit(_))它虽然能产生Par[Int]但每个值都只是Par.unit包装的常量——没有任何fork也没有嵌套结构。用它测试fork相关的性质时所有测试用例长得一模一样几乎测不到并行执行路径。练习 16 的要求正是补上这一环构造一个Gen[Par[Int]]让生成出的并行程序在结构上有真实变化。仓库中该练习的答案位于 answerkey/testing/16.answer.md其注释清楚地说明了设计意图AGen[Par[Int]]generated from a list summation that spawns a new parallel computation for each element of the input list summed to produce the final result. This is not the most compelling example, but it provides at least some variation in structure to use for testing.即生成一个“列表求和”的并行程序输入列表的每个元素都会催生一个新的并行计算例子本身不惊艳但足以在结构上产生变化供测试使用。值得一提的是练习 16 没有提供单独提示answerkey/testing/16.hint.md 内容仅为-按 README.md 的说明无提示的题目提示文件就是一个-而练习 17 的提示则直接点明了依赖关系“使用上一道练习的Gen[Par[Int]]生成器”。二、标准答案逐行拆解练习 16 的完整答案如下val gpy2: Gen[Par[Int]] choose(-100, 100).listOfN(choose(0, 20)).map(ys ys.foldLeft(Par.unit(0))((p, y) Par.fork(p.map2(Par.unit(y))(_ _))))整条流水线由三个环节组成环节一choose(-100, 100)——元素取值域生成[-100, 100)区间内的随机Int右开区间不含 100。仓库答案模块 Exhaustive.scala 中的实现是def choose(start: Int, stopExclusive: Int): Gen[Int] Gen(State(RNG.nonNegativeInt).map(n start n % (stopExclusive-start)), bounded(LazyList.from(start).take(stopExclusive-start)))随机通道用State[RNG, Int]完成底层是第 6 章的线性同余RNG.Simple见 State.scala穷举通道则生成从start起、长度为stopExclusive - start的有限惰性列表即该有限域内全部 200 个整数。环节二listOfN(choose(0, 20))——动态决定列表长度listOfN有两个重载固定长度版listOfN(n: Int)和动态长度版listOfN(size: Gen[Int])。这里传入的是一个Gen[Int]因此每次采样都会先随机出一个[0, 20)的长度再生成相应个数的元素。动态版本的本质是flatMapdef listOfN(size: Gen[Int]): Gen[List[A]] size.flatMap(n this.listOfN(n))见 Exhaustive.scala这一层是“结构变化”的第一来源测试用例的列表长度从 0 到 19 随机浮动后续并行程序的规模也随之变化。环节三map(ys foldLeft(...))——把列表折叠成并行求和程序这是核心。对采样出的列表ys用foldLeft从Par.unit(0)起步逐个元素把y加进并行程序ys.foldLeft(Par.unit(0))((p, y) Par.fork(p.map2(Par.unit(y))(_ _)))其中Par.unit(0)零元素基线立即返回常量 0见 Par.scala它不占用ExecutorService线程p.map2(Par.unit(y))(_ _)把当前累积结果p与常量y并行合并求和。阻塞版map2的实现是同时取出两个Future的结果再应用函数f见 Par.scalaPar.fork(...)把合并动作提交到线程池异步执行。阻塞版fork的实现是es.submit(new Callable[A] { def call a(es).get })见 Par.scala。整个生成器用到的组合子及其作用汇总如下组合子 / API作用答案模块中的位置choose(-100, 100)生成[-100, 100)的随机 Int穷举通道覆盖全 200 个值Exhaustive.scalalistOfN(choose(0, 20))动态长度019的列表生成Exhaustive.scalamap同时映射随机通道与穷举通道Exhaustive.scalaPar.unit(0)常量并行值作为折叠起点Par.scalaPar.map2(_, _)(_ _)合并两个并行结果Par.scalaPar.fork提交到线程池控制并行的唯一入口Par.scala三、它生成的计算长什么样一棵“求和分叉树”以ys List(1, 2, 3)为例展开折叠过程p0 Par.unit(0) p1 Par.fork(p0.map2(Par.unit(1))(_ _)) // 结果1 p2 Par.fork(p1.map2(Par.unit(2))(_ _)) // 结果3 p3 Par.fork(p2.map2(Par.unit(3))(_ _)) // 结果6最终p3是一个右嵌套right-nested的分叉链每遇到一个元素就在现有程序外包一层fork(map2(..., unit(y)))。因此fork数量 列表长度列表越长并行任务越多运行时要占用的线程池线程也越多每个fork恰好包住一个map2map2本身不开新线程fork是 API 中控制并行的唯一函数这一点在 Par.scala 的注释中有明确说明并行度全部来自fork计算结果恒等于ys.sum无论结构如何变化最终值都是列表元素之和这为断言提供了天然的不变式。换句话说gpy2生成的不是“随机的Par值”而是“参数化结构的求和程序”——长度变化带来嵌套深度与任务数量的变化元素取值变化带来计算内容的变化二者叠加测试输入就具备了足够的多样性。四、配套练习 17用 forkProp 验证 fork 律练习 16 的生成器不是摆设它正是练习 17 的主角。练习 17 的答案位于 answerkey/testing/17.answer.md只有一行val forkProp Prop.forAllPar(gpy2)(y equal(Par.fork(y), y))这条属性表达的是并行库的一条核心定律——fork 律把任意并行程序y再用fork包一层其结果应当与原来完全相同fork只引入异步调度不应改变语义。其中两个关键拼图在答案模块 Exhaustive.scala 中均有定义def equalA: Par[Boolean] p.map2(p2)(_ _) // L150-L151把两个 Par 的比较包装成 Par[Boolean] def forAllParA(f: A Par[Boolean]): Prop forAll(executors ** g)((s, a) f(a).run(s).get) // L163-L164equal复用map2把“比较相等”这件事也并行化forAllPar则把“执行器”和“被测值”打包成二元组一起生成对每个用例先用a构造出Par[Boolean]再在随机选出的执行器s上run(...).get取出布尔结果交给forAll判定。forkProp的完整定义与gpy2一起位于 Exhaustive.scalapint2语义相同、放在object Gen内的版本见 Exhaustive.scala。执行器也要“随机生成”forAllPar里的executors本身就是一个Gen[ExecutorService]位于 Exhaustive.scalaval executors: Gen[ExecutorService] weighted( choose(1,4).map(Executors.newFixedThreadPool) - .75, unit(Executors.newCachedThreadPool) - .25)75% 的概率生成 14 线程的定长线程池newFixedThreadPool25% 的概率生成缓存线程池newCachedThreadPool。这样fork律就不只在线程数充足时成立还要在线程池很小1 个线程fork大量排队时依然成立——这正是属性测试优于手写示例的地方调度环境本身也被纳入被测维度。weighted的实现含穷举通道的加权交错interleave见 Exhaustive.scala。值得一提的是equal(Par.fork(y), y)中的y来自gpy2其结构里已经含有多层fork再套一层fork后两侧程序的嵌套深度不同但求和结果必须一致。如果fork实现有并发 bug例如竞态导致部分子任务未被等待这类深层嵌套的用例比gpy生成的平凡unit用例更容易暴露问题。五、底层原理Gen 的“穷举 随机”双重表示gpy2能同时服务于随机测试与穷举测试是因为答案模块中的Gen采用双通道表示见 Exhaustive.scalacase class GenAsample随机采样通道基于第 6 章的State[RNG, A]exhaustive穷举通道LazyList[Option[A]]None表示“域在此处穷尽”。关键设计决策有穷域穷举、无穷域退化为Nonechoose(-100, 100)的穷举通道是bounded(LazyList.from(start).take(200))——完整的 200 个值而double、int等无穷域则直接使用unbounded LazyList(None)Exhaustive.scala。组合子双通道同步推进map同时映射两个通道Exhaustive.scalalistOfN的穷举通道用cartesian生成“所有元素组合”的笛卡尔积Exhaustive.scalaflatMap遇到None会转入无穷域Exhaustive.scala。因此gpy2的穷举通道在理论上覆盖了“所有长度 × 所有元素组合”的完整程序结构。穷举与随机按 1:2 混跑forAllExhaustive.scala把用例数分成两份——先用n/3个用例遍历穷举通道若有限域被完整走完且无反例则属性直接Proven否则转入随机通道补足剩余用例最终标记为Unfalsified。这解释了为什么forkProp这类用例可以在“有限但天文数字级大”的域上快速给出结论它只检查前几十个穷举用例 随机补采不会真的枚举200^19种组合。正因为穷举通道可能是天量惰性流gpy2在答案模块中被声明为lazy valExhaustive.scala把构造推迟到首次使用时避免object Prop初始化阶段就背负构造开销。六、在仓库中运行与验证仓库采用 Scala CLI 构建用法见 README.md。先在根目录编译scala-cli compile .然后用 REPL 直接检验forkPropscala-cli console .import fpinscala.answers.testing.exhaustive.* import fpinscala.answers.testing.exhaustive.Prop.* forkProp.run() // 默认 maxSize100, testCases100若属性通过run会按 Exhaustive.scala 的输出逻辑打印 OK, property unfalsified, ran 100 tests.gpy2的穷举域过大33 个穷举用例无法走完故状态为Unfalsified而非Provenrun的默认参数与check一致见 Exhaustive.scala。若想拿到结构化结果而非打印可改用forkProp.check()。仓库的测试套件同样使用了这套穷举版Gen库来验证并行组合子例如 NonblockingSuite.scala 里用Gen.boolean.map(Par.unit)、genShortNumber.map(Par.unit)等生成Par[Int]、Par[String]、Par[Par[String]]再对choice、choiceN、join等逐一断言GenSuite.scala 则用ExhGen.int、ExhGen.int ** ExhGen.int等生成器对Gen原语本身做属性测试。二者的公共基类 PropSuite.scala 把forAll的结果桥接到 munit 测试框架。七、延伸从Par到函数生成练习 1820gpy2的套路组合子流水线 结构变化可以继续推广。练习 18 指出想验证takeWhile的最长前缀性质需要Gen[Int Boolean]这类函数生成器练习 19 的答案给出了通用方案——引入Cogen反生成器用输入值扰动 RNG 种子来构造确定性函数见 answerkey/testing/19.answer.md 与 Exhaustive.scala 的cogenInt、takeWhilePropIntdef cogenInt: Cogen[Int] (i, rng) val (seed, rng2) rng.nextInt RNG.Simple(seed.toLong ^ i.toLong)练习 20 则是开放题仓库未给出固定答案answerkey/testing/20.answer.md。可以看到第 8 章从“生成基本值”一路走到“生成程序结构Par”“生成函数”最终把属性测试的生成能力覆盖到了高阶对象——gpy2正是这条进阶路径上承上启下的关键一环。参考与深入阅读练习 16 答案answerkey/testing/16.answer.md练习 17 答案answerkey/testing/17.answer.md穷举版测试库完整实现含gpy2、forkProp、forAllPar、executors、Gen双通道表示Exhaustive.scala非穷举版答案库更贴近书中正文的Gen State[RNG, A]表示Gen.scala并行库Par阻塞版fork/map2/unitPar.scala非阻塞 CPS 版Nonblocking.scala配套测试NonblockingSuite.scala、GenSuite.scala、PropSuite.scala构建与运行方式README.md赞分享示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载相关推荐fpinscala 习题解析用 asyncF 将同步函数提升为 Par 并行计算fpinscala 习题解析用 asyncF 将同步函数提升为 Par 并行计算 在《Functional Programming in Scala》第 7示例工程fpinscala 第七章并行组合子用 sequence / sequenceBalanced 把 List[Par[A]] 规约为 Par[List[A]]fpinscala 第七章并行组合子用 sequence / sequenceBalanced 把 List Par A 规约为 Par List A 本篇文示例工程exa 完全指南Rust 编写的现代 ls 替代品从命令行选项到源码级原理exa 完全指南Rust 编写的现代 ls 替代品从命令行选项到源码级原理 exa 是一个用 Rust 编写、以现代 ls 替代品为目标的开源命令行工具示例工程上一篇炉石传说增强插件架构深度解析与实战配置指南下一篇XUnity.AutoTranslator5分钟解锁Unity游戏实时翻译的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考