ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Effect 中 `Function.memoizeIdempotent` 的设计原理:幂等对象变换的固定点缓存与 Schema AST 去重优化

Effect 中 `Function.memoizeIdempotent` 的设计原理:幂等对象变换的固定点缓存与 Schema AST 去重优化 Effect 中Function.memoizeIdempotent的设计原理幂等对象变换的固定点缓存与 Schema AST 去重优化【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect本篇文章基于当前仓库Effect 4.0核心包版本为4.0.0-rc.115中的变更记录 .changeset/pre/memoize-idempotent-asts.md 展开。该变更以patch级别引入Function.memoizeIdempotent并将其应用于 Schema AST 的规范化处理包括optional/mutable属性修饰符的幂等变换同时为 Config 的 cursor AST 编译增加缓存。读完本文你将理解什么是幂等idempotent对象变换、memoizeIdempotent与既有memoize在语义上的本质区别、其固定点缓存的实现机制以及它是如何被用于避免 Schema AST 被反复加工、提升 Schema 构造与 Config 解析性能的。一、变更背景为什么要幂等的 memoizationEffect 的Schema模块内部维护一棵不可变的 Schema AST抽象语法树。大量高层 API如Schema.Struct、Schema.optionalKey、Schema.mutableKey、Schema.toType、Schema.toEncoded在底层都会反复对同一棵 AST 节点进行变换例如标记某个属性为可选isOptional标记某个属性为可变isMutable剥离或翻转编码链encoding将 AST 归一化为 union 选择时的候选类型。这些变换有一个共同特征对同一输入重复应用得到的结果在对象身份identity上是可复用的——第一次变换产出的结果再次变换仍是它自己或等价的规范化结果。这就是幂等变换f(f(x))与f(x)在语义上等价且结果对象本身可以被视为不动点fixed point。memoizeIdempotent正是为这类场景设计的它不仅能缓存输入 → 输出的映射还能把输出本身也登记为缓存键从而把每个已计算的结果当作固定点来复用。二、memoizeIdempotent的源码实现该 API 定义在 packages/effect/src/Function.ts#L1350-L1385属于category cachingsince 4.0.0/** * Creates a memoized idempotent object transformation that caches both inputs * and their outputs by object identity. * * **When to use** * * Use when an object transformation is idempotent and its output can be safely * reused as a fixed point. * * **Details** * * After computing an input, the returned function caches both the input and * the output. Calling it with either reference returns the output without * invoking the supplied function again. * * **Gotchas** * * The returned function treats each computed output as a fixed point. If * applying the supplied function to an output would produce an observably * different value, this memoization changes that behavior. * * see {link memoize} for memoizing functions without an idempotence requirement * category caching * since 4.0.0 */ export function memoizeIdempotentA extends object(f: (a: A) A): (a: A) A { const cache new WeakMapA, A() return (a) { const cached cache.get(a) if (cached ! undefined) return cached const result f(a) cache.set(a, result) cache.set(result, result) return result } }关键点有三基于对象身份的WeakMap缓存以输入对象的引用为键命中则直接返回缓存结果不再调用f。由于使用WeakMap缓存不会阻止输入/输出对象被垃圾回收长期运行也不会造成内存泄漏。双方向登记计算完result f(a)后同时执行cache.set(a, result)与cache.set(result, result)。前者是常规 memoization后者则把result自身登记为键、自身为值——这意味着任何后续以该输出对象为输入甚至以任意与其身份相同的引用为输入的调用都会直接命中缓存。undefined语义与memoize一致参见 packages/effect/src/Function.ts#L1328-L1339 中对memoize的说明undefined被保留用来表示缓存未命中因此不承诺支持返回undefined的变换。从类型签名f: (a: A) A也能看出本 API 仅适用于对象到对象的变换。与memoize的语义对比紧邻其上的memoizepackages/effect/src/Function.ts#L1339签名更宽export function memoizeA extends object, O extends {} | null(f: (a: A) O): (ast: A) O { const cache new WeakMapobject, O() return (a) { const cached cache.get(a) if (cached ! undefined) return cached const result f(a) cache.set(a, result) return result } }两者差异的本质是是否把输出当作固定点维度memoizememoizeIdempotent缓存方向仅缓存 输入 → 输出同时缓存 输入 → 输出 与 输出 → 输出输出要求任意对象或null必须是A类型对象且变换幂等用输出再调用会再次执行f直接命中缓存不执行f适用场景一般性防重复计算幂等变换、规范化、AST 去重Gotcha使用陷阱memoizeIdempotent把每个计算出的输出都当作固定点。如果你的变换函数对某个输出再次应用时会产出可观察的不同结果即并非真正幂等那么 memoization 会悄悄改变原有行为。因此它只应作用于结果再次变换等于自身的函数。三、测试用例如何验证固定点语义变更配套的测试位于 packages/effect/test/Function.test.ts#L334-L361两个用例分别验证了固定点缓存的两个方向describe(memoizeIdempotent, () { it(caches the output as a fixed point, () { let callCount 0 const input { id: input } const output { id: output } const f F.memoizeIdempotent((obj: { id: string }) { callCount return obj input ? output : obj }) assert.strictEqual(f(input), output) assert.strictEqual(f(output), output) assert.strictEqual(callCount, 1) }) it(caches an input that is already a fixed point, () { let callCount 0 const f F.memoizeIdempotent((obj: object) { callCount return obj }) const input {} assert.strictEqual(f(input), input) assert.strictEqual(f(input), input) assert.strictEqual(callCount, 1) }) })第一个用例caches the output as a fixed point验证了双方向登记f(input)计算出output后f(output)不再执行用户函数callCount保持为 1直接返回已缓存的output——这就是输出即固定点。第二个用例caches an input that is already a fixed point验证了平凡场景当变换函数返回输入本身恒等变换本身就是固定点时重复调用同样只执行一次。四、在 Schema AST 中的落地避免规范化处理被反复执行变更记录明确指出memoizeIdempotent被用来避免重复处理 canonical Schema ASTs包括 optional 和 mutable 属性修饰符。在 packages/effect/src/SchemaAST.ts 中可以找到多处应用1.optionalKey与mutableKey属性修饰符的幂等变换Schema AST 的Context携带isOptional与isMutable两个布尔标记见 packages/effect/src/SchemaAST.ts#L580-L626并有配套的读取辅助函数isOptional/isMutablepackages/effect/src/SchemaAST.ts#L4502-L4516。optionalKey将字段标记为可选export const optionalKey: A extends AST(ast: A) A memoizeIdempotent(A extends AST(ast: A): A { const context ast.context ? ast.context.isOptional false ? new Context(true, ast.context.isMutable, ast.context.constructorDefault, ast.context.annotations) : ast.context : new Context(true, false) return optionalKeyLastLink(replaceContext(ast, context)) })mutableKey将字段标记为可变非readonlyexport const mutableKey memoizeIdempotent(A extends AST(ast: A): A { const context ast.context ? ast.context.isMutable false ? new Context(ast.context.isOptional, true, ast.context.constructorDefault, ast.context.annotations) : ast.context : new Context(false, true) return mutableKeyLastLink(replaceContext(ast, context)) })两处源码分别见 packages/effect/src/SchemaAST.ts#L4384-L4391 与 packages/effect/src/SchemaAST.ts#L4401-L4408。这里的幂等性一目了然一个已经带isOptional true上下文的 AST再次应用optionalKey会命中ast.context.isOptional false ? ... : ast.context分支直接返回原上下文不会产生新的Context对象。用memoizeIdempotent包装后这种重复应用无变化的调用直接从缓存返回避免了对同一棵 AST 反复构造新节点。这些底层函数对应着Schema层的公开 APISchema.optionalKeypackages/effect/src/Schema.ts#L2317、Schema.requiredKeypackages/effect/src/Schema.ts#L2336、Schema.mutableKeypackages/effect/src/Schema.ts#L2442、Schema.readonlyKeypackages/effect/src/Schema.ts#L2461以及类型层的Optionality required | optional描述packages/effect/src/Schema.ts#L87-L97。典型使用方式来自Schema.optionalKey的文档示例packages/effect/src/Schema.ts#L2300-L2312import { Schema } from effect const schema Schema.Struct({ name: Schema.String, age: Schema.optionalKey(Schema.Number) }) // Type: { readonly name: string; readonly age?: number } type Person typeof schema[Type]2.toType/toEncoded编码链的规范化toType负责剥离 AST 上的编码链返回类型侧的 ASTtoEncoded则是先flip再toType。两者都用memoizeIdempotent包装packages/effect/src/SchemaAST.ts#L4558 与 packages/effect/src/SchemaAST.ts#L4605-L4607export const toType memoizeIdempotent(A extends AST(ast: A): A { if (ast.encoding) { return toType(replaceEncoding(ast, undefined)) } // ... 递归规整 合并 encodingChecks 到 checks }) export const toEncoded memoizeIdempotent((ast: AST): AST { return toType(flip(ast)) })由于toType是幂等的已经去掉编码的 AST 再次toType不会变化memoizeIdempotent能保证当toType的递归规整路径上出现同一节点时直接复用已有结果避免重复的深度遍历。3.toCandidateunion 选择候选类型的归一化toCandidatepackages/effect/src/SchemaAST.ts#L3263-L3276用于 union 解码时把成员 AST 归一化为候选类型如string、number、array等。它同样用memoizeIdempotent包装并在循环中递归调用ast.recur?.(toCandidate, identity)保证共享子节点只被处理一次。4. 通用工具applyToSelfOrLastLinkEncodingIdempotentSchemaAST 内部还提供了一个基于memoizeIdempotent的通用组合子packages/effect/src/SchemaAST.ts#L4302-L4314export function applyToSelfOrLastLinkEncodingIdempotent( f: (ast: AST) AST, options?: { readonly stopAt?: (link: Link) boolean } ) { function out(ast: AST): AST { if (ast.encoding) { const last ast.encoding[ast.encoding.length - 1] return options?.stopAt?.(last) ? ast : replaceEncoding(ast, updateLastLink(ast.encoding, out)) } return f(ast) } return memoizeIdempotent(out) }它被用于parameterFromPropertyKey、parameterFromString、partFromString等属性键参数归一化packages/effect/src/SchemaAST.ts#L4741-L4776这些函数负责把索引签名参数、Symbol、Number、Literal 等节点转换为对应的 StringTree 编码。使用memoizeIdempotent后同一 AST 节点的参数归一化结果会被复用避免反复执行ast.toCodecStringTree()这类较重的转换。五、Config 侧缓存 cursor AST 编译变更记录的另一半是Cache Config schema cursor AST compilation——为 Config 的 cursor AST 编译加缓存。Config.schema是 Effect Config 中通过 Schema 定义结构化配置的核心入口packages/effect/src/Config.ts#L877-L883export function schemaT(codec: Schema.ConstraintCodecT, unknown, path?: string | ConfigProvider.Path): ConfigT { const codecStringTree Schema.toCodecStringTree(codec) const encodedAst SchemaAST.toEncoded(codecStringTree.ast) const decodeCursor SchemaParser.decodeUnknownEffect( Schema.makeSchema.CodecT, ConfigCursor(toConfigCursorAST(codecStringTree.ast)) ) const localPath typeof path string ? [path] : path ?? [] // ... }调用链的关键节点先把 codec 转为canonicalStringTree形式Schema.toCodecStringTree其编码形状决定了 provider 数据如何加载标量 schema 读相邻标量值对象 schema 读声明属性与匹配的 record 键数组 schema 读索引子项见 packages/effect/src/Config.ts#L800-L828 的文档说明。再通过toConfigCursorAST把该 AST编译为游标解码用的 AST——解码时每个节点会拿到一个ConfigCursorpackages/effect/src/Config.ts#L663-L668它携带provider、path、node与toString解码过程即沿配置路径逐层定位 provider 节点loadCursor/loadChildCursorpackages/effect/src/Config.ts#L672-L682。最后用SchemaParser.decodeUnknownEffect构造出实际执行解码的decodeCursor。toConfigCursorAST本身用memoize做了 AST 级缓存packages/effect/src/Config.ts#L730const toConfigCursorAST memoize((root: SchemaAST.AST): SchemaAST.AST { const seen new WeakSetSchemaAST.AST() const recur SchemaAST.applyToSelfOrLastLinkEncoding((ast) { seen.add(ast) switch (ast._tag) { case Objects: { /* 收集属性键与匹配的索引签名键 */ } case Arrays: { /* 按索引读取子节点 */ } // ... } }) // ... })值得注意的是memoizeIdempotent与既有memoize在这里是配套使用的toConfigCursorAST整体用memoize缓存同一棵根 AST 只编译一次其内部递归用的applyToSelfOrLastLinkEncoding也带有 memoization。由于Config.schema在构造时会同步执行Schema.toCodecStringTree、SchemaAST.toEncoded与toConfigCursorAST而这些步骤都落在上一节提到的幂等缓存之上同一个 Schema 被多个Config.schema调用复用时canonical AST 的规范化和 cursor AST 的编译都只会发生一次。六、实际收益与使用建议收益减少重复计算与对象分配从实现层面看本变更带来的收益主要体现在三方面避免重复处理 canonical Schema ASToptionalKey/mutableKey/toType/toEncoded/toCandidate等高频规范化操作对同一节点只执行一次后续调用走WeakMap命中减少新对象分配幂等变换命中缓存后不再new Context、不再构造替换后的 AST 节点降低 GC 压力Config 解析提速Config.schema构造期的 StringTree 规整与 cursor AST 编译被缓存同一 Schema 复用于多个配置项时开销摊薄。使用建议仅对真正幂等的对象变换使用memoizeIdempotent要求f(f(x))与f(x)语义等价且输出可作为固定点复用若变换不满足幂等或输出可能为undefined/null请改用一般的memoize它也不支持undefined返回值或自行实现缓存由于缓存基于对象身份WeakMap 引用键结构相等但身份不同的对象不会共享缓存项——这在 Schema AST 场景中恰好是期望行为因为 AST 节点以引用相等作为规范化判据缓存本身是惰性且无界的按活跃对象数量增长但因为WeakMap不持有强引用配合不可变 AST 的使用方式不会造成泄漏。七、小结Function.memoizeIdempotent是 Effect 4.0 在函数式缓存原语上的一处小而关键的补充它以幂等变换 固定点缓存为语义用两行cache.set实现了输入/输出双向登记让结果再次变换等于自身的规范化函数可以安全地复用输出。这一原语被立刻应用到 Schema AST 的optionalKey、mutableKey、toType、toEncoded、toCandidate等高频路径并为Config.schema的 cursor AST 编译提供了缓存支撑。对于需要处理大量共享 AST 节点、或希望在应用层实现幂等规范化缓存的开发者这套实现与配套测试packages/effect/test/Function.test.ts#L334-L361都是可直接参考的范本。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表