Effect BigDecimal 聚合运算新能力:sumAll 与 multiplyAll 源码解析与实战
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
本文围绕 Effect 中BigDecimal模块新增的sumAll与multiplyAll两个聚合 API 展开,介绍其在金额、数量等高精度十进制场景下的使用方式、底层实现原理,以及它与Number、BigInt同族 API 的对齐关系。读者阅读后可以掌握如何对一组成语十进制值做批量求和与求积,并能理解其空集合语义、零值短路优化等实现细节。
变更背景:一次与Number、BigInt的功能对齐
本仓库的变更记录 .changeset/pre/add-bigdecimal-sumall-multiplyall.md 记录了这样一次版本变更(effect包,patch级别):
Added
BigDecimal.sumAllandBigDecimal.multiplyAllfor feature parity withNumberandBigInt, closes #1880.
翻译过来即:为BigDecimal模块新增sumAll与multiplyAll两个函数,目的是与Number、BigInt两个模块保持特性对齐,并关闭 issue #1880。
在此之前,BigDecimal只提供了二元运算sum(相加)与multiply(相乘),开发者若要对一个数组或任意可迭代集合中的多个十进制值做累计求和/求积,只能手动reduce,代码冗长且容易在空集合等边界情况下出错。而Number与BigInt模块早已具备sumAll/multiplyAll,本次变更正是补齐了这一缺口。
BigDecimal 的内部表示:value 与 scale
要理解两个新 API 的实现,先要了解BigDecimal的数据结构。在 packages/effect/src/BigDecimal.ts 中,BigDecimal接口由两部分构成:
value: bigint:无缩放整数(unscaled value),即去掉小数点后的整数部分;scale: number:小数位数,即小数点后保留的位数。
例如字符串"123.45"会被表示为value = 12345n, scale = 2;"42"则是value = 42n, scale = 0。这种“bigint + scale”的存储方式正是BigDecimal能避免 JavaScriptnumber浮点表示误差(如0.1 + 0.2 !== 0.3)的关键:所有数字都落在bigint上运算,不存在二进制浮点精度丢失。
正因如此,模块文档(packages/effect/src/BigDecimal.ts)将BigDecimal定位为“当 JavaScriptnumber的舍入精度不够时使用的十进制数与算术模块”,典型场景包括金额(money)、数量(quantities)与测量值(measurements)。
sumAll:对一组 BigDecimal 批量求和
使用方式
sumAll接受一个Iterable<BigDecimal>,返回这些值的总和(单个BigDecimal):
import { BigDecimal } from "effect" const result = BigDecimal.sumAll([ BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("4") ]) // => BigDecimal.fromBigInt(9n)它同样适用于任意可迭代对象,包括Set、生成器等,只要迭代产出的是BigDecimal即可。对于金额累计、订单明细小计合并等场景,使用sumAll可以完全绕开 JavaScriptnumber的中间转换,始终保持十进制精度。
空集合与零值语义
sumAll对空集合返回加法的单位元zero,即数值0(value = 0n, scale = 0)。这一点与数学直觉一致:空集之和为零。测试用例 packages/effect/test/BigDecimal.test.ts 明确验证了三种情形:
assertEquals(BigDecimal.sumAll([]), $("0")) assertEquals(BigDecimal.sumAll([$("2"), $("3"), $("4")]), $("9")) assertEquals(BigDecimal.sumAll([$("1.5"), $("-1.5")]), $("0"))其中$是测试文件中的便捷构造函数(内部调用fromStringUnsafe)。第三个用例说明正负相消时结果同样归零。
实现原理
sumAll的源码位于 packages/effect/src/BigDecimal.ts:
export const sumAll = (collection: Iterable<BigDecimal>): BigDecimal => { let out: BigDecimal = zero for (const n of collection) { out = sum(out, n) } return out }其实现非常简洁:以zero为累计器初始值,遍历集合逐个调用二元sum累加。之所以能保证精度,是因为sum在 源码 中会对不同scale的操作数先做scale对齐(把较小 scale 的值放大到与较大 scale 一致),再对bigint部分做加法,最终结果的scale取两者较大者。例如测试中的$("3.00000") + $("50") = $("53")、$("1.23") + $("0.0045678") = $("1.2345678"),小数位数都得到了完整保留。
从实现可以看出一个可以放心使用的特性:结果的精度等于参与累加的所有值中最大的 scale,中间过程不会因为进位或对齐丢失小数位。
multiplyAll:对一组 BigDecimal 批量求积
使用方式
multiplyAll接受一个Iterable<BigDecimal>,返回所有值的乘积(单个BigDecimal):
import { BigDecimal } from "effect" const result = BigDecimal.multiplyAll([ BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("4") ]) // => BigDecimal.fromBigInt(24n)空集合与零值语义
与sumAll不同,multiplyAll对空集合返回乘法的单位元one,即数值1。测试用例 packages/effect/test/BigDecimal.test.ts 验证:
assertEquals(BigDecimal.multiplyAll([]), $("1")) assertEquals(BigDecimal.multiplyAll([$("2"), $("3"), $("4")]), $("24")) assertEquals(BigDecimal.multiplyAll([$("2"), $("0"), $("4")]), $("0"))注意sumAll的空集结果是0而multiplyAll的空集结果是1——这是加法与乘法单位元的自然延伸,属于易踩坑点,务必区分。
实现原理与零值短路优化
multiplyAll的源码位于 packages/effect/src/BigDecimal.ts:
export const multiplyAll = (collection: Iterable<BigDecimal>): BigDecimal => { let out: BigDecimal = one for (const n of collection) { if (n.value === bigint0) { return zero } out = multiply(out, n) } return out }它的实现比sumAll多了一个值得注意的优化:零值短路。一旦遍历中发现某个元素的value === 0n,立即返回zero,不再继续迭代后续元素。这是因为任何数与0相乘结果都是0,提前返回既能节省后续乘法开销,也保证了正确性(测试用例$("2") × $("0") × $("4") = $("0")即验证此路径)。
multiply本身(源码)的实现为make(self.value * that.value, self.scale + that.scale):bigint部分直接相乘,scale部分相加。因此multiplyAll结果的 scale 是参与累乘的所有值 scale 之和,例如$("3") × $("0.5") = $("1.5")(scale 从0 + 1 = 1)。
与 Number、BigInt 同族 API 的对齐关系
本次变更的动机是“feature parity”(特性对齐),三个模块的 API 形状完全一致,均接受Iterable并返回聚合结果:
| 模块 | 求和 API | 求积 API | 空集合结果 |
|---|---|---|---|
Number | Number.sumAll(iterable) | Number.multiplyAll(iterable) | 0/1 |
BigInt | BigInt.sumAll(iterable) | BigInt.multiplyAll(iterable) | 0n/1n |
BigDecimal | BigDecimal.sumAll(iterable) | BigDecimal.multiplyAll(iterable) | 0/1 |
从源码看,Number.sumAll(packages/effect/src/Number.ts)与Number.multiplyAll(packages/effect/src/Number.ts)的实现与BigDecimal版本如出一辙——multiplyAll同样包含n === 0时的提前返回;BigInt模块的sumAll/multiplyAll(packages/effect/src/BigInt.ts)也保持同样的语义。
三个模块的一致性带来一个实际好处:如果你的业务代码在Number/BigInt/BigDecimal之间做类型迁移(例如先用普通数字原型验证,再切换为高精度十进制),聚合逻辑几乎可以一字不改地平移,只需替换导入模块。这也正是 changelog 中“feature parity”所承诺的开发体验。
实战:订单金额批量结算
结合以上特性,看一个贴近真实业务的例子——多行订单明细的金额汇总与折扣计算:
import { BigDecimal } from "effect" // 订单行金额(单位:元),全部用字符串构造避免浮点误差 const lineTotals = [ BigDecimal.fromStringUnsafe("19.99"), BigDecimal.fromStringUnsafe("5.50"), BigDecimal.fromStringUnsafe("129.00") ] // 汇总订单金额 => 154.49,精度保留到分 const subtotal = BigDecimal.sumAll(lineTotals) // 全场九折 => 139.041 const discount = BigDecimal.fromStringUnsafe("0.9") const total = BigDecimal.multiplyAll([subtotal, discount]) console.log(BigDecimal.format(total)) // => "139.041"其中BigDecimal.format(packages/effect/src/BigDecimal.ts)负责把内部的value+scale表示渲染回人类可读的十进制字符串,适合最终展示或落库前的序列化。
小结
BigDecimal.sumAll与BigDecimal.multiplyAll是 EffectBigDecimal模块为补齐与Number、BigInt特性对齐而新增的一对聚合 API。它们都接受任意Iterable<BigDecimal>并返回单个BigDecimal结果,区别在于:
sumAll以zero为初值逐个累加,空集合返回0;multiplyAll以one为初值逐个累乘,空集合返回1,并内置零值短路优化。
借助value + scale的内部表示,两者在整个聚合过程中始终运行在bigint上,精度由参与运算的十进制值决定,完全规避 JavaScript 浮点误差。需要进一步验证行为细节的读者,可以直接阅读对应的测试用例 packages/effect/test/BigDecimal.test.ts,或深入源码 packages/effect/src/BigDecimal.ts 查看完整实现。
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考