资讯动态

Schema toArbitrary 迁移指南:基于 effect-smol 的 v4 任意值生成 API 深度解析

发布时间:2026/9/15 17:25:12 来源:尧图企业网站定制
Schema toArbitrary 迁移指南基于 effect-smol 的 v4 任意值生成 API 深度解析【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3codeSchema.toArbitrary是 Effect 生态中把 Schema 描述自动派生为 fast-check 属性测试任意值生成器Arbitrary的核心能力。本仓库 effect-smol.repos/effect-smol目录下的 Effect 源码在其 4.0 版本中对这一 API 做了大规模重构引入新的 filter 元数据、候选生成器candidate generation、可选的派生报告derivation reports与递归感知生成recursion-aware generation并将旧的OrderedConstraintT模型重命名。本文以变更说明文档 update-schema-arbitrary-report.md 为主线结合 toArbitrary.ts、Schema.ts 与 toArbitrary.test.ts 的实现细节系统讲解从旧 API 迁移到新模型所需的全部步骤帮助读者安全完成升级并理解底层推导原理。一、变更背景一次针对任意值生成的系统性重构在旧版本中toArbitrary的推导链路由多个彼此独立的机制拼凑而成过滤器通过toArbitraryConstraint注解携带约束、分桶式bucketed约束分散在字符串/数组/数字/日期等多个命名空间中、递归 schema 通过context.isSuspend手工判断、声明钩子只能返回裸的FastCheck.ArbitraryT。这次重构的目标是把上述机制统一为四套协同工作的新概念filter 元数据filter metadata过滤器可通过arbitrary注解同时携带constraint约束提示与candidate候选生成源候选生成candidate generation为无法用约束描述取值空间的过滤器提供带权重的额外生成源且所有候选仍须通过过滤器校验可选的派生报告optional derivation reportstoArbitrary(schema, { report: true })返回{ value, report }把生成器与派生过程说明解耦递归感知生成recursion-aware generation以context.recursion取代context.isSuspend配合{ arbitrary, terminal }双分支输出解决递归 schema 的有限终止问题。从源码结构看核心实现集中在 toArbitrary.ts它通过memoized函数对SchemaAST.AST做缓存recur递归遍历 AST、base按节点类型分发到底层生成器同时维护suspendDepthIdentifierMapWeakMapSchemaAST.Suspend, FastCheck.DepthIdentifier这是递归感知生成的实现基础。二、迁移第一步filter 注解从toArbitraryConstraint到arbitrary命名空间旧 API 中若要为一个过滤器附带约束提示写法是Schema.filter(Schema.String, (s) s.length 3, { toArbitraryConstraint: constraint // 旧直接挂约束对象 })新 API 把所有与任意值生成相关的提示统一收进arbitrary命名空间分为两种形态// 形态一可以表达为约束时 Schema.filter(Schema.String, (s) s.length 3, { arbitrary: { constraint } }) // 形态二无法用约束描述取值空间时提供一个加权候选源 Schema.filter(Schema.String, (s) s.startsWith(t3), { arbitrary: { candidate: { weight: 2, // 可选默认 1必须是正整数 make: (fc, context) fc.string({ minLength: 2 }).map((s) t3 s) } } })底层语义candidate 与 filter 的执行顺序在 toArbitrary.ts 中applyFilterLayer函数明确规定了执行顺序先合并候选源再统一跑过滤器。applyCandidates的实现细节包括基础生成器的权重固定为1每个候选通过candidate.make(fc, ctx)创建make接收合并后的当前节点约束Context可以返回undefined表示放弃包括在递归终止分支中主动退出权重必须为正整数否则抛出arbitraryError(a candidate with an invalid weight)只有 1 个候选时直接返回该生成器多个候选时用fc.oneof(...weighted)合并。类型层面见 Schema.tsexport interface Filter { readonly constraint?: GenerationConstraint | undefined readonly candidate?: Candidate | undefined } export interface Candidate { readonly weight?: number | undefined readonly make: ( fc: typeof FastCheck, context: Context ) FastCheck.Arbitraryunknown | undefined }关键结论候选值仍会被每个 schema 过滤器校验。无效候选只会影响生成效率不会产生非法值——这是候选与直接覆盖生成器的本质区别。三、迁移第二步分桶约束 → 扁平化GenerationConstraint旧 API 把约束按类型分桶存放新 API 统一收敛为扁平的Schema.Annotations.ToArbitrary.Constraint即GenerationConstraint结构。完整映射关系如下旧的约束写法新的扁平字段适用节点string.minLengthminLength字符串长度下限array.minLengthminLength数组长度下限对象属性个数 / 集合大小minLength对象、Set、Map、Hash 集合、Chunk 的基数下限string.maxLengthmaxLength字符串长度上限array.maxLengthmaxLength数组长度上限对象属性个数 / 集合大小maxLength对象、集合的基数上限string.patternspatterns字符串匹配的正则模式集合number.isIntegerinteger只生成整数number.noNaNnoNaN禁止生成 NaNnumber.noDefaultInfinitynoInfinity禁止生成 ±Infinitydate.noInvalidDatevalid日期必须是有效日期array.comparator去重unique使用 Effect 相等性去重ordered.min/minExcluded/max/maxExcludedordered.minimum/exclusiveMinimum/maximum/exclusiveMaximum有序值数字、BigInt、DateTime、BigDecimal 等的范围GenerationConstraint的完整类型定义Schema.tsexport interface GenerationConstraint { readonly minLength?: number | undefined readonly maxLength?: number | undefined readonly patterns?: readonly [string, ...Arraystring] readonly integer?: boolean | undefined readonly noInfinity?: boolean | undefined readonly noNaN?: boolean | undefined readonly unique?: boolean | undefined readonly ordered?: OrderedConstraintany | undefined }需要特别注意的是array.comparator的去重语义在新模型下统一为unique相等性判定固定使用 Effect 的Equal.equals而不是用户自定义 comparator。这在 toArbitrary.ts 的arrayWithConstraints中可以看到其comparator参数由constraint?.unique ? Equal.equals : undefined决定最终落到fc.uniqueArray。底层实现约束如何在各类型生成器中消费GenerationConstraint是节点局部的生成提示不是自描述的校验契约。每个生成器只消费它认识的字段其余字段被忽略最终 schema 过滤器仍会校验每个生成值。核心生成逻辑见base函数String有patterns时对每个模式执行fc.stringMatching(new RegExp(pattern))并用fc.oneof合并否则用lengthToFastCheckConstraints把minLength/maxLength翻译为 fast-check 的长度约束Numberinteger为真时走fc.integer否则fc.float范围来自constraint.ordered且要求order Order.NumberArraylengthToFastCheckConstraints映射长度约束validateArrayConstraints在minLength maxLength时抛出Unable to derive an arbitrary for array constraints。有序约束的合并与OrderedConstraintT重命名新模型中范围约束统一收敛到ordered字段类型更名为OrderedConstraintTSchema.tsexport interface OrderedConstraintT { readonly order: Order.OrderT readonly minimum?: T | undefined readonly exclusiveMinimum?: boolean | undefined readonly maximum?: T | undefined readonly exclusiveMaximum?: boolean | undefined }从 toArbitrary.ts 的mergeOrderedConstraints可以看到合并规则两个有序约束的order实例不同则快速失败抛出Cannot merge ordered arbitrary constraints with different Order instances上下界分别通过mergeOrderedBound按Order比较取更紧的边界takeComparison -1取更小下界、 1取更大上界边界相等时保留排他性更强的标记selfExclusive || thatExclusive。生成器只在识别出order时才消费这些约束如Order.Number、Order.BigInt、DateTime、BigDecimal例如 BigDecimal 生成器中的bigDecimalMaxScale就是基于ordered计算最大小数位。四、迁移第三步arbitrary 钩子上下文从constraints/isSuspend到constraint/recursion旧 API 在 arbitrary 钩子中读取context.constraints复数获取全部约束、用context.isSuspend判断是否处于递归挂起分支。新 API 中读取context.constraint单数——它是当前节点合并后的单个GenerationConstraint用context.recursion取代context.isSuspend——它是一个递归预算对象。Recursion与Context的类型定义Schema.tsexport interface Recursion { readonly maxDepth: number readonly depthIdentifier: FastCheck.DepthIdentifier | string } export interface Context { readonly constraint?: ToArbitrary.GenerationConstraint | undefined readonly recursion?: ToArbitrary.Recursion | undefined }递归感知生成的正确写法terminal 分支在前文档给出了明确的组合范式同时存在有限terminal分支与递归分支时把context.recursion传给fc.oneof且有限分支放在第一位。原因见Recursion的文档注释fast-check 一旦对depthIdentifier达到maxDepth就只使用第一个分支因此把有限分支放首位可以保证生成必然终止。该用法在测试中有直接验证toArbitrary.test.tsarbitrary: ctx.recursion undefined ? arbitrary : fc.oneof(ctx.recursion, terminal, arbitrary)底层实现上toArbitrary.ts 用suspendDepthIdentifierMap为每个SchemaAST.Suspend节点缓存唯一的fc.createDepthIdentifier()并以RecursionStackReadonlyArraySchemaAST.Suspend跟踪当前递归路径recur在遇到 Suspend 时把recursion注入上下文同时在recursionStack中记录该节点从而识别递归环。五、迁移第四步声明钩子输出{ arbitrary, terminal }双分支旧 API 中声明declaration钩子统一返回FastCheck.ArbitraryT。新 API 引入规范化输出形态原子atomic声明仍可返回裸的FastCheck.ArbitraryTnormalizeDerivation会自动包装泛型generic声明当能够保留有限终止分支时应返回{ arbitrary, terminal }。normalizeDerivation的实现逻辑toArbitrary.ts概括如下裸 arbitrary 且无类型参数 →terminal自动取自身原子声明天然可终止裸 arbitrary 且有类型参数 →terminal为undefined无法保证泛型分支有限视为不可终止{ arbitrary, terminal }对象 → 显式使用terminal字段缺省时按上述规则回退。这些terminal分支最终通过makeTypeParameters以{ arbitrary, terminal }的形式传给泛型声明钩子供钩子在构造递归 alternative 时选用。递归感知的推导路径recur中的derive函数正是通过normalizeDerivation(...)[lazyNormal ? terminal : arbitrary]在正常生成与终止分支之间切换。六、迁移第五步report选项与toArbitraryLazy新 API 在入口函数上引入了可选的派生报告// 开启报告返回 { value, report } const { value, report } Schema.toArbitrary(schema, { report: true }) // 默认行为不变直接返回 arbitrary const arbitrary Schema.toArbitrary(schema) // 惰性版本始终返回惰性 arbitrary const lazyArbitrary Schema.toArbitraryLazy(schema)toArbitrary(schema, { report: true })→{ value, report }value是生成器、report是派生过程说明不带{ report: true }时保持旧语义直接返回生成器不破坏既有调用点toArbitraryLazy始终返回惰性 arbitrary调用时才真正执行派生适合需要延迟实例化的场景。入口函数签名见 Schema.tsexport function toArbitraryS extends Constraint(schema: S): ArbitraryS[Type] { const lawc InternalArbitrary.memoized(schema.ast) return (fc) lawc(fc, {}) }可以看到默认入口通过InternalArbitrary.memoized对 AST 做缓存——相同 schema 的重复调用是廉价的。memoized memoize((ast) recur(ast, []))以WeakMapSchemaAST.AST, LazyArbitraryWithContextany为缓存载体。七、实战示例一次完整的迁移以3 个以上字符、以 t3 开头的字符串过滤器为例展示旧新 API 的完整对照import { Schema } from effect import * as FastCheck from fast-check // ---- 旧 API ---- const oldSchema Schema.filter(Schema.String, (s) s.startsWith(t3), { toArbitraryConstraint: { // 旧字符串约束挂在此处无法表达前缀语义 minLength: 3 } }) // ---- 新 API ---- const newSchema Schema.filter(Schema.String, (s) s.startsWith(t3), { arbitrary: { // 前缀 t3 无法描述为约束改用加权候选源filter 仍会校验 candidate: { weight: 1, make: (fc) fc.string({ minLength: 2 }).map((s) t3${s}) } } }) // 开启派生报告 const { value, report } Schema.toArbitrary(newSchema, { report: true }) console.log(FastCheck.sample(value, 5)) // 全部以 t3 开头 console.log(report)再以一个递归数据类型为例展示{ arbitrary, terminal }与recursion的配合import { Schema } from effect const treeSchema Schema.recursive(() Schema.Union( Schema.Literal(leaf), // 有限终止分支 Schema.Struct({ children: Schema.Array(treeSchema) }) // 递归分支 ) ) const makeTreeArbitrary Schema.toArbitrary(treeSchema) const TreeArbitrary makeTreeArbitrary(FastCheck) FastCheck.sample(TreeArbitrary, 10) // 总是能在有限深度内终止底层对该递归结构的处理逻辑为recur进入 Suspend 节点时从suspendDepthIdentifierMap取深度标识并注入context.recursion终止分支使用array函数的 terminal 模式maxLength被压缩为minLength ?? 0即递归层不产出子元素从而保证fc.oneof(ctx.recursion, terminal, recursive)一定能在maxDepth内落地到有限分支。八、从源码结构可以进一步推断的要点结合 toArbitrary.ts 的整体结构可以观察到以下设计意图约束合并是收紧而非叠加combiner对minLength取max、对maxLength取min、对布尔型约束取or、对patterns做连接合并保证来自多个过滤器的提示合并后不会互相矛盾不可派生的节点会显式报错base对Never与无注解的Declaration抛出Unsupported AST ...避免静默产生错误生成器类型参数与惰性求值结合makeTypeParameters中泛型参数的 normal 分支通过fc.constant(null).chain(() tp(...))延迟求值而 terminal 分支直接求值避免递归环上的无限展开测试覆盖了关键边界toArbitrary.test.ts 中专门的recursion × constraint guards测试组验证递归与约束组合的代码路径可作为迁移后回归验证的参考。九、迁移检查清单检查项旧写法新写法过滤器约束注解toArbitraryConstraint: constraintarbitrary: { constraint }无法描述为约束的过滤器无对应机制arbitrary: { candidate: { weight?, make } }分桶约束string.minLength/number.isInteger/date.noInvalidDate等扁平GenerationConstraint见第三节映射表有序范围ordered.min/minExcluded/max/maxExcludedordered.minimum/exclusiveMinimum/maximum/exclusiveMaximum类型更名为OrderedConstraintT钩子上下文约束context.constraintscontext.constraint递归判断context.isSuspendcontext.recursion配合fc.oneof(ctx.recursion, terminal, recursive)且有限分支在前声明钩子返回裸FastCheck.ArbitraryT原子声明可保留裸值泛型声明返回{ arbitrary, terminal }派生报告无toArbitrary(schema, { report: true })→{ value, report }toArbitraryLazy恒返回惰性 arbitrary延伸阅读变更说明原始文档update-schema-arbitrary-report.md位于.changeset/pre预发布目录effect: patch级别的变更集核心实现toArbitrary.tsAPI 类型与入口函数Schema.tsToArbitrary命名空间、toArbitrary、toArbitraryLazy测试用例toArbitrary.test.ts变更集管理配置config.json 与 pre.json【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价