Scala 15:错误短路与错误累积,校验顺序怎样决定结果
商品为空、数量不是数字,应该返回几条错误
订单表单同时提交空商品标识与数量字符串 “bad”。如果使用 Either.flatMap 串起来,商品检查先失败,数量检查不会执行,结果只有商品错误。若界面希望一次展示所有独立字段问题,就需要另一种组合方式:两个检查都运行,失败信息按规定顺序合并。
但“尽量返回所有错误”不能无限制推广。数量没有解析成整数之前,库存是否足够没有可靠输入。用零代替坏字符串再检查库存,会制造一条并不描述原始问题的错误。独立校验可以累积,依赖校验需要等待前置成功,区分这两种关系比选择哪个容器更重要。
本文使用 Scala 3.3.7、标准库 2.13.16、JDK 21,不引入效果库。最小累积组合器只负责两个已经完成的结果;它不负责并行、取消、超时或网络重试。实验同时检查返回内容、字段顺序和回调次数,避免把“错误列表类型”误解成“自动累积所有错误”。
先让错误保留字段位置与稳定代码
1 | |
field 表示输入位置,code 表示稳定错误类别。界面可以根据这两项选择语言文案,日志可以按代码统计,测试也不必依赖会修改的中文提示。若把错误只写成一段句子,字段定位、国际化和程序化处理都会依赖不稳定文本。
Vector 允许按顺序保存多条错误,但类型别名本身并不保证 Left 中至少有一条,也不保证字段名合法。本例的构造函数每次创建单条错误,组合函数只拼接已有列表,因此在本例路径上不会生成空错误列表;外部仍能手动写 Left(Vector.empty)。需要更强约束时,应封装非空结构,而不是从别名推导保证。
错误代码也不需要包含原始敏感输入。记录字段和值的全部细节可能泄露用户数据;保留必要位置与类别通常足够展示修复方向。业务确实需要诊断原文时,应明确脱敏和保存范围。错误建模解决表示,不会自动决定信息暴露政策。
字段内部的依赖仍然需要短路
1 | |
解析失败时,范围校验没有 q 可以判断,所以返回 malformed,并跳过后续步骤。解析成功但值为零时,返回 non-positive。这两条错误不是彼此独立:第二条的前提是第一步已经产生整数,因此不应该对 “bad” 同时返回“格式错误”和“非正数”。
商品字段检查则不依赖数量,数量检查也不需要商品标识。因此两者可以分别完成,再把结果合并。把字段内部链路和字段之间组合分开,能够同时保留短路的合理性与表单累积的完整性,不必强行让整个校验流程只有一种策略。
如果业务还要检查某商品对应库存,库存判断依赖已验证商品和数量。合理的结构是先累积这些独立字段错误,全部成功后形成 Order,再进入库存或权限步骤。这样每个阶段接收的类型都体现已经满足的前提,避免给后续逻辑传入占位值。
普通 flatMap 为什么只留下第一条错误
1 | |
product 返回 Left 后,Either 的 flatMap 不调用成功回调。计数器仍为零,quantity 根本没有在这条链路中执行。即使失败类型是 Vector[FieldError],这个行为也不会改变:容器内部保存列表,和组合器是否执行后续检查,是两个不同层面。
这份短路有时正是目标。例如商品标识决定后续远程服务的地址,前置失败之后继续发请求没有意义。问题出在把同样结构用于独立表单字段,却希望自动看到所有错误。语法 for 也不会改变它,for 最终仍使用这些 map、flatMap 调用。
如果先把两个校验保存成 val,再用 flatMap 组合,两个计算都会先执行,但最终 Either 仍只保留按照链路首先出现的 Left。求值完整性和错误信息完整性要分别验证。不能只看到计数器为二,就断言返回值包含两条错误。
相反,采用按名参数的自定义组合器也不一定累积,因为它可以选择不求值第二个参数。判断时应同时看参数求值方式与分支逻辑。本文的 zip 接收普通严格参数,两个参数表达式先形成结果,再进入四分支组合。
最小累积组合器只有四种情况
1 | |
两边成功时,结果保存两个成功值的积;两边失败时,结果按左边在前、右边在后的顺序连接错误;只有一边失败时,保留那一边的错误。四个分支覆盖两种输入结果的全部组合,行为可以直接列成二乘二表。
这个函数不尝试在一边失败时构造半个 Order。成功结果要求商品和数量都存在,失败则只返回错误。若界面需要显示部分已解析字段,应另设计包含部分结果的类型,不能把这里的成功类型塞进 null 或默认值来凑齐构造参数。
错误顺序来自 x ++ y,不是任意集合自带的业务顺序。把参数交换,或者把拼接写成 y ++ x,就会改变展示顺序。本章固定商品在前、数量在后,并直接断言错误向量相等。使用 Set 去重则会改变重复与顺序语义,不能当作无影响的实现替换。
zip 的签名也不要求两个字段类型相同。商品成功是 String,数量成功是 Int,组合结果是二元组,最后才能用 Order 构造函数转换。这种先组合独立结果、后构造领域对象的安排,把对象完整性的前提放在一个清楚的边界上。
正常、单错、双错都需要验收
正常程序验证商品 p1 与数量二得到 Right(Order(“p1”,2))。双错输入得到两条错误,顺序严格为商品 empty、数量 malformed。两种单错情况也分别测试,确认 zip 没有只处理“全成功或全失败”而遗漏一边失败。
quantity(“0”) 单独得到 non-positive,说明范围错误与解析错误没有合并成笼统失败。短路链计数为零,说明依赖回调没有被错误执行。返回值、顺序、执行次数组成三个不同的可观察条件,任何一个缺失都可能放过错误实现。
编译反例把 Either[String, Int] 直接传给要求 Int 的库存判断。失败说明“可能得到数量”不是“已经得到数量”。修复应在成功分支中调用库存逻辑,或者通过 flatMap 把成功值传进去;强制转换或默认零都会绕过原本需要表达的前提。
这些测试没有验证远程库存一致性。即使全部字段合法,真正提交时库存也可能已被其他订单消费。字段校验、业务状态检查与原子提交是不同阶段,错误累积不能让它们变成同一个静态事实。必要时应让提交接口再次验证可变业务条件。
跨字段规则应在共同前提成立后执行
假设订单还包含起止时间,需要检查结束不早于开始。两个时间字符串的格式解析可以独立累积;顺序比较则需要两个时间都成功解析。若一端格式错误,返回“开始晚于结束”没有事实依据,可能误导用户修复错误字段。
订单折扣同理。折扣上限可能依赖已验证金额,金额又依赖数量和单价。可以先分别累积独立解析错误,再把成功结果传给金额计算,最后检查折扣。这个依赖图通常比一条超长 for 更清楚,也比“所有错误一次返回”这种过宽目标更可执行。
需要返回全部可确定错误时,可以把每条校验的前提写清楚:没有前提的检查都能运行,依赖成功值的检查只在该值存在时运行。这样既不会遗漏独立字段,也不会产生由占位值推导的伪错误。累积目标应限定为当前信息足以判断的错误集合。
相同字段还可能有多条独立规则,例如字符串长度和禁止字符都可直接检查原文。这时是否同时返回两条提示,取决于产品交互;它不由 Either 或 zip 自动决定。重要的是每条规则都针对真实输入,而不是以前一条失败后的虚构值继续检查。
扩展到多字段时保持顺序与构造边界
三个字段可以先 zip 前两个,再与第三个 zip,最后把嵌套元组转换成对象。错误连接若使用稳定的顺序操作,能够保留约定字段次序;但成功值的嵌套形状会变复杂,后续可以引入小型 map2 或 map3 帮助构造。
抽象的需求应来自重复代码,而不是在第一个表单就建立通用校验框架。当前 zip 的四分支已经足够暴露语义;若增加通用操作,仍需保持正常、单错、双错及顺序断言。方法名叫 accumulate 并不能替代这些行为证据。
还要限制错误输出规模。批量文件有百万行非法数据时,完整保存每条错误可能耗尽内存。可以规定前若干条详细错误加总计,或分批输出带行号的结果。这个限制属于交付协议,不应通过未报告就丢弃列表尾部实现。调用方需要知道结果是否被截断。
纯字段校验通常可以安全重复,但昂贵远程验证不应无意重复。把校验函数装入 View 或在多处调用,可能增加请求次数。本文严格参数 zip 不负责缓存也不负责调度;需要限制执行次数,应另加可观测测试而不是从“累积”一词推导。
手算与修改练习
手算:将 zip(quantity(“bad”), product(“”)) 的错误连接顺序保持为左后右,结果第一条属于哪个字段?答案是 quantity,因为参数顺序改变了。若界面必须按固定表单顺序展示,应固定组合顺序或在输出阶段按显式字段序号整理。
修改练习:增加邮箱字段,空值与缺少分隔符返回不同代码。先独立校验商品、数量和邮箱,再组合成完整对象。双错与三错测试都要检查具体字段顺序;正常输入要验证所有成功字段保留。参考思路是 zip(zip(product,quantity),email),再解构嵌套元组,不能把失败字段用空字符串代入构造器。
进一步增加“数量大于十时必须填写备注”规则。备注是否必填依赖合法数量,应在数量解析成功后检查;数量 malformed 时不应凭默认值推导备注规则。答案应同时保留独立邮箱错误与可确定的备注错误,并说明哪些依赖未满足而没有执行。
实验记录与依据
源码为 examples/scala-lab/snippets/15/Chapter15.scala,入口 scalaexamples.Chapter15。实际日志、命令与退出码见 RUN.md。
- Either 2.13.16 API:核对右偏短路行为。
- Vector 2.13.16 API:核对有序错误保存与连接。
- Scala 模式匹配教程:核对结果分支组合。四分支 zip、依赖划分与字段错误策略由本章独立定义,不声称是标准库自带累积协议。
前置阅读:Option、Either 与 Try。
