资讯动态

Scala函数式数据处理:过滤排序分组聚合的思维重构

发布时间:2026/10/13 12:08:00 来源:尧图企业网站定制
1. 这不是“学语法”而是重构你写代码的思维习惯很多人点开“Scala进阶”系列心里想的是“又一个讲for循环怎么写、case class怎么定义的教程”——错了。今天这篇要聊的根本不是“怎么用Scala写Java风格的代码”而是彻底切换你的编程范式从“告诉机器一步步做什么”转向“描述数据该变成什么样子”。函数式编程FP在Scala里不是可选项它是让代码从“能跑”走向“可演进”的分水岭。我带过不少从Java转来的开发者他们第一周写Scala代码里全是var、while、mutable.ListBuffer还美其名曰“先写出来再重构”。结果呢三个月后同一个模块改三次需求每次都要重读二十行嵌套if-else调试时得在IntelliJ里打七八个断点就为了搞清某个中间变量在第几层map里被悄悄改了。这不是技术问题是思维惯性在拖后腿。标题里列的四个动作——过滤、排序、分组、聚合——表面看是集合操作API实则是一套完整的数据流建模语言。比如filter(_.status active)它不只删掉几行数据它在声明“我只关心活跃状态的实体”这个声明本身就成了后续所有逻辑的契约。而groupBy(_.category).mapValues(_.size)这行代码本质上是在构建一张轻量级的内存维度表和你在BI工具里拖拽“按品类统计数量”是同一思维——只是这里没有界面只有纯数据关系。关键词里虽然空着但整篇内容锚定三个不可绕过的内核不可变性Immutability、高阶函数Higher-Order Functions、组合子Combinators。它们不是概念名词而是你每天敲代码时手指要记住的肌肉记忆。比如sorted方法背后不是简单的快排调用而是依赖Ordering隐式参数完成类型安全的比较groupBy返回的Map[K, List[V]]结构天然规避了Java里常见的MapK, ListV手动判空、初始化、add逻辑——这些细节不是语法糖是编译器在帮你把边界条件写进类型系统里。适合谁读如果你写过1000行以上Scala但还在为“为什么List.map返回新List却总担心性能”而查文档如果你的团队Code Review常出现“这里用val还是var”的争论如果你发现同样的业务逻辑在不同服务里写了四遍略有差异的for循环——那这篇就是为你写的。它不教你怎么“学会”而是带你亲手拆开Scala集合库的齿轮看清每个齿形如何咬合出可维护性。2. 过滤从“删掉不要的”到“定义要保留的契约”过滤操作看似最简单filter一个方法就能搞定但恰恰是这里埋着最多认知陷阱。新手常犯的错是把过滤写成“排除法”list.filter(x !x.isExpired x.amount 0 x.type ! TEMP)。这行代码读起来像在念咒语而且每加一个条件逻辑复杂度就指数级上升。真正的函数式思维是把过滤条件升华为领域契约Domain Contract。2.1 契约优先用类型和命名固化业务规则我们来看一个真实案例。某电商系统需要筛选“可结算订单”原始代码是这样的val validOrders orders.filter { order order.status PAID order.paymentTime.isBefore(Instant.now().minusSeconds(300)) order.items.nonEmpty order.totalAmount BigDecimal(0) }这段代码的问题在于所有业务规则都挤在lambda里无法复用、无法测试、无法被其他开发者一眼理解。重构后的做法是——把契约变成独立的、可命名的函数// 定义清晰的契约函数 val isSettleable: Order Boolean { order order.status PAID order.paymentTime.isBefore(Instant.now().minusSeconds(300)) order.items.nonEmpty order.totalAmount BigDecimal(0) } val validOrders orders.filter(isSettleable)这看起来只是提取变量但本质变化是isSettleable现在是一个一等公民你可以把它传给其他函数可以单独写单元测试甚至可以在日志里打印Applying settleable contract to ${order.id}。更重要的是当产品说“结算时间窗口从5分钟改成10分钟”你只需要改isSettleable函数体而不是在十几个filter调用处逐个搜索。提示契约函数命名必须带动词前缀如isXxx,canXxx,shouldXxx这是团队协作的隐形协议。看到isRefundable你就知道它返回Boolean且代表业务可行性而不是某个字段的getter。2.2 复合契约用and/or组合子替代嵌套逻辑单个契约容易写但现实业务往往需要组合。比如“可结算订单”还需满足“非测试环境订单”或“VIP用户订单”。如果用传统写法// ❌ 反模式嵌套条件导致可读性崩塌 orders.filter { order (order.status PAID order.paymentTime.isBefore(...)) || (order.userId.startsWith(VIP_) order.totalAmount BigDecimal(1000)) }正确解法是利用Scala的函数组合能力// ✅ 正确用and/or组合已定义的契约 val isPaidAndTimely isSettleable // 复用上文定义 val isVipHighValue (order: Order) order.userId.startsWith(VIP_) order.totalAmount BigDecimal(1000) val isEligible isPaidAndTimely.or(isVipHighValue) val eligibleOrders orders.filter(isEligible)这里or是Function1[A, B]自带的方法它接受另一个函数并返回新函数。这种写法的优势在于每个子契约依然保持单一职责组合逻辑清晰可见且isEligible本身又可被其他模块复用。我见过一个风控系统把27个业务规则拆成原子契约再用and/or/negate动态组装成不同场景的过滤器上线半年没改过一行核心过滤逻辑。2.3 性能暗坑filter vs withFilter的编译器魔法很多开发者不知道filter在for推导式里会被编译器自动优化为withFilter。看这个例子// 写法A显式filter for { order - orders.filter(_.status PAID) item - order.items.filter(_.price 100) } yield (order.id, item.name) // 写法B等价的for推导式编译后完全一样 for { order - orders if order.status PAID item - order.items if item.price 100 } yield (order.id, item.name)关键区别在于filter会立即创建新集合而withFilter返回的是一个惰性视图LazyList只有在真正需要元素时才计算。当处理百万级订单列表时filter可能瞬间生成几十万中间List而withFilter全程只持有引用。实测数据某物流轨迹查询服务将filter改为withFilter后GC暂停时间从80ms降到12ms。注意withFilter仅对支持它的集合有效List、Vector、Stream等Array不支持。若不确定用view.filter(...).toList显式创建惰性视图更稳妥。3. 排序从“比大小”到“定义全序关系”排序在命令式编程里是Collections.sort(list, comparator)而在Scala函数式世界它本质是为类型建立数学意义上的全序Total Order。sorted方法背后不是算法选择而是类型系统如何保证“任意两个元素都能比较”。3.1 隐式Ordering编译器强制的契约一致性看这个常见错误case class Product(name: String, price: BigDecimal, category: String) val products List(Product(A, 10.5, ELEC), Product(B, 8.2, BOOK)) val sorted products.sorted // ❌ 编译失败报错信息是No implicit Ordering defined for Product。这不是Scala在刁难你而是在问“你打算按什么规则排序价格名称还是类别价格的复合规则”——它拒绝给你一个默认答案因为默认答案往往就是bug的温床。解决方案是提供隐式Ordering// 方案1按价格升序最常用 implicit val byPrice: Ordering[Product] Ordering.by(_.price) // 方案2按名称降序 implicit val byNameDesc: Ordering[Product] Ordering.by(_.name).reverse // 方案3复合排序先按类别再按价格 implicit val byCategoryThenPrice: Ordering[Product] Ordering.by((p: Product) (p.category, p.price))重点来了这些Ordering实例不是随便写的。Ordering.by(_.price)会自动生成一个Ordering[BigDecimal]而BigDecimal的Ordering是Scala标准库预定义的——这意味着你的排序逻辑天然继承了BigDecimal的精确比较语义不会像Double那样有浮点误差。这就是类型系统在帮你兜底。3.2 动态排序运行时决定排序策略业务常需要“用户选择按价格/销量/评分排序”。命令式写法是if-else分支函数式写法是把排序策略变成可传递的值sealed trait SortBy case object PriceAsc extends SortBy case object PriceDesc extends SortBy case object SalesDesc extends SortBy def sortProducts(products: List[Product], strategy: SortBy): List[Product] { val ordering strategy match { case PriceAsc Ordering.by(_.price) case PriceDesc Ordering.by(_.price).reverse case SalesDesc Ordering.by(_.salesCount).reverse } products.sorted(ordering) // 显式传入ordering }这个设计的威力在于排序逻辑完全解耦。你可以轻松添加SalesDesc策略而无需修改sortProducts函数体前端传来的SortBy枚举直接映射到类型安全的排序行为连字符串解析错误都避免了。3.3 稳定性陷阱sorted vs sortBy的底层差异sorted和sortBy都返回有序列表但实现机制天差地别sorted要求元素类型有Ordering内部调用java.util.Arrays.sortTimsort稳定排序Stable SortsortBy接受A B函数先对每个元素计算key如_.price再按key排序也是稳定排序等等都是稳定排序那区别在哪看这个例子case class User(name: String, age: Int) val users List(User(Alice, 25), User(Bob, 30), User(Charlie, 25)) // 按年龄排序同龄人保持原顺序 users.sortBy(_.age) // List(Alice, Charlie, Bob) — Alice在Charlie前 users.sortBy(-_.age) // List(Bob, Alice, Charlie) — Bob最大Alice和Charlie同龄保持原序关键点sortBy的稳定性体现在“相同key的元素保持输入顺序”而sorted的稳定性是“相同元素保持输入顺序”。当你的排序key有重复时如多人同龄sortBy的行为更符合业务直觉。我曾在一个用户管理后台踩过坑用sorted按状态分组后同状态用户顺序随机导致前端分页时同一页出现重复用户——换成sortBy(_.status)立刻解决。实测技巧对大数据集排序sortBy通常比sorted快15%-20%因为sortBy的key计算是单次遍历而sorted需多次比较调用getter。但若key计算开销大如调用外部API则应先map缓存key再排序。4. 分组从“建Map”到“定义维度切片”groupBy是Scala里最被低估的函数。很多人把它当成“Java的stream.collect(Collectors.groupingBy())”只看到它返回Map[K, List[V]]却忽略了它本质是在数据集上施加一个维度投影Dimensional Projection。4.1 分组键的设计哲学K必须是“可穷举、可比较、可序列化”的值分组键K的选择直接决定后续聚合的难度。反模式案例// ❌ 错误用Date作为key导致相同日期的Date对象不相等 orders.groupBy(_.createdAt.toLocalDate) // LocalDate是值类型安全 // ❌ 更糟用自定义类作为key但没重写equals/hashCode case class OrderKey(category: String, region: String) orders.groupBy(order OrderKey(order.category, order.region)) // ❌ 编译通过但分组失效正确做法永远是用不可变、无副作用的值类型String, Int, LocalDate, Enum作为key。如果必须用复合键用元组// ✅ 推荐元组天然支持equals/hashCode val grouped orders.groupBy(o (o.category, o.region)) // grouped: Map[(String, String), List[Order]]元组的好处是零成本抽象——(String, String)就是Tuple2[String, String]编译器自动生成正确的hashCode和equals。某金融系统曾因自定义key类未重写hashCode导致分组后Map只有1个entry所有key哈希冲突线上故障持续2小时。4.2 分组后的数据形态List[V]不是终点而是聚合起点新手常止步于groupBy拿到Map[K, List[V]]就去遍历。但函数式思维要求你立刻追问“这个List要变成什么”——是计数求和取最新转换为JSONmapValues就是你的聚合入口// 场景统计各品类订单数 val countByCategory orders.groupBy(_.category).mapValues(_.size) // 场景计算各品类平均金额 val avgByCategory orders.groupBy(_.category) .mapValues { ordersInCat ordersInCat.map(_.totalAmount).sum / ordersInCat.size } // 场景取各品类最新订单按支付时间 val latestByCategory orders.groupBy(_.category) .mapValues { ordersInCat ordersInCat.maxBy(_.paymentTime) }注意mapValues的惰性特性它返回的是MapView不会立即计算。当你调用countByCategory(ELEC)时才真正执行_.size。这对大数据集是福音但若需多次访问同一value应转为严格集合val strictMap countByCategory.toMap // 立即计算所有size4.3 高级分组用groupMap和groupMapReduce减少中间集合Scala 2.13引入groupMap和groupMapReduce它们能一步完成分组映射聚合避免创建冗余的List[V]// 传统写法创建中间List val sumByCategory orders.groupBy(_.category) .mapValues(_.map(_.totalAmount).sum) // groupMapReduce写法零中间List val sumByCategory2 orders.groupMapReduce(_.category)(_.totalAmount)(_ _) // 参数1分组key函数 // 参数2每个元素映射为聚合值这里是totalAmount // 参数3聚合函数这里是加法性能对比10万订单方法内存占用执行时间groupBy mapValues42MB186msgroupMapReduce28MB112ms节省33%内存和40%时间。原理很简单groupMapReduce在遍历原始集合时直接对每个元素计算key和value然后累加到对应key的桶中全程不构建List[V]。某实时报表服务迁移后JVM Young GC频率下降60%。踩坑提醒groupMapReduce的聚合函数必须是结合律Associative的即(a b) c a (b c)。加法、乘法、max、min都满足但字符串拼接a b c不满足因顺序敏感此时应改用groupMap。5. 聚合从“算总数”到“构建数据立方体”聚合Aggregation是函数式数据处理的皇冠。reduce、fold、scan这些方法不是“高级for循环”而是在数据流上定义代数结构Algebraic Structure的工具。理解它们的区别决定了你能否写出既正确又高效的代码。5.1 reduce vs fold空集合的哲学分歧reduce和fold都用于归约但设计哲学截然不同reduce: “把一堆东西合并成一个前提是至少有一个”fold: “从一个初始值开始把一堆东西合并成一个即使一个都没有”看这个致命错误val amounts List.empty[BigDecimal] amounts.reduce(_ _) // ❌ 运行时抛出UnsupportedOperationException: empty.reduce amounts.fold(BigDecimal(0))(_ _) // ✅ 返回0reduce的签名是def reduce[B : A](op: (B, A) B): B它要求集合非空fold的签名是def fold[B](z: B)(op: (B, A) B): Bz是零值Identity Element。在加法中零值是0在乘法中是1在字符串拼接中是。业务中空集合极其常见如新用户无订单、促销期无销量。用reduce就像开车不系安全带——99%的时间没事但出事就是大事。我见过一个支付对账服务因某天零交易reduce崩溃导致整个批次失败人工补单耗时3小时。5.2 foldLeft vs foldRight栈溢出与尾递归的实战权衡foldLeft和foldRight的区别常被简化为“左结合vs右结合”但实际影响远超结合性// foldLeft从左到右尾递归优化安全 List(1,2,3,4).foldLeft(0)(_ _) // ((01)2)3)4 10 // foldRight从右到左非尾递归大数据集栈溢出 List(1,2,3,4).foldRight(0)(_ _) // 1(2(3(40))) 10foldRight在List上会先展开所有递归调用再逐层返回10万元素必然StackOverflowError。解决方案有两个优先用foldLeft绝大多数聚合求和、计数、连接都可用foldLeft重写用reverse.foldLeft替代foldRightlist.reverse.foldLeft(z)(op)语义等价且安全但注意reverse会创建新List若内存敏感可用ListBufferdef safeFoldRight[A, B](list: List[A], z: B)(op: (A, B) B): B { val buf new ListBuffer[B]() buf z list.foreach { a val prev buf.last buf op(a, prev) } buf.last }5.3 scan聚合的“历史快照”功能scan系列scan,scanLeft,scanRight是被严重低估的神器。它不只返回最终结果而是返回每一步聚合的中间状态val numbers List(1,2,3,4) numbers.scan(0)(_ _) // List(0, 1, 3, 6, 10) —— 前缀和数组 numbers.scanLeft(0)(_ _) // 同上 numbers.scanRight(0)(_ _) // List(10, 9, 7, 4, 0) —— 后缀和这有什么用举个硬核案例实时风控中的滑动窗口统计。需求是“过去10分钟内同一IP的请求次数超过100次则告警”。传统方案用定时任务每秒扫描而函数式方案是// 假设requests是按时间排序的请求流 case class Request(ip: String, timestamp: Instant) val recentRequests requests.takeWhile(_.timestamp.isAfter(Instant.now().minusSeconds(600))) // 计算每个请求时刻的“当前窗口内请求数” val cumulativeCounts recentRequests .scanLeft(0) { (count, req) // 伪代码检查req.timestamp是否在窗口内是则count1 if (req.timestamp.isAfter(Instant.now().minusSeconds(600))) count 1 else count } // 找出第一个超限点 val alertPoint cumulativeCounts.indexWhere(_ 100)scan在这里的价值是一次遍历生成完整的历史序列避免了N次重复计算。某广告平台用此方案将QPS 5000的风控延迟从200ms压到18ms。6. 组合技用管道Pipeline串联四大操作单个操作讲透了真正的威力在于像搭积木一样组合它们。函数式编程的终极目标是构建可读、可测、可复用的数据处理管道Data Processing Pipeline。6.1 构建端到端管道从原始数据到业务指标我们以电商后台的“热销品类日报”为例展示四大操作如何无缝衔接// 原始订单流可能来自Kafka或DB val rawOrders: List[Order] ??? // 管道定义清晰的业务语义流 val hotCategoriesReport rawOrders // Step 1: 过滤 —— 只看已支付、非测试订单 .filter { order order.status PAID !order.orderId.startsWith(TEST_) } // Step 2: 排序 —— 按支付时间倒序确保最新订单在前 .sortBy(-_.paymentTime.toEpochMilli) // Step 3: 分组 —— 按品类分组 .groupBy(_.category) // Step 4: 聚合 —— 计算各品类指标 .mapValues { ordersInCat val totalRevenue ordersInCat.map(_.totalAmount).sum val orderCount ordersInCat.size val avgOrderValue if (orderCount 0) totalRevenue / orderCount else BigDecimal(0) HotCategory( category ordersInCat.head.category, revenue totalRevenue, count orderCount, aov avgOrderValue, topProducts ordersInCat .sortBy(-_.totalAmount) // 再次排序取Top3 .take(3) .map(_.productId) .toList ) } // Step 5: 转为报告格式额外操作体现管道延展性 .toSeq .sortBy(-_._2.revenue) // 按营收降序 .map { case (category, report) report }这个管道的精妙之处在于每一步都有明确业务含义filter是业务准入sortBy是时效性保障groupBy是维度切片mapValues是指标计算无中间变量污染不需要val filteredOrders,val sortedOrders等临时变量代码即文档易于测试你可以单独测试filter部分给定订单列表验证输出是否符合契约也可以测试整个管道给定样本数据验证报告结构易于监控在每一步后插入.tap(println)或.tap(log.info)即可查看各阶段数据形态6.2 管道的健壮性设计错误处理与降级生产环境不能假设数据完美。管道需内置容错// 方案1用Try包裹可能失败的操作 import scala.util.{Try, Success, Failure} val safePipeline rawOrders .filter { order Try(order.totalAmount BigDecimal(0)).getOrElse(false) // 即使totalAmount为null也安全 } .map { order Try { // 可能抛异常的计算 order.items.map(_.price * _.quantity).sum }.getOrElse(BigDecimal(0)) } // 方案2用Option做空值防护 val ordersWithItems rawOrders .filter(_.items.nonEmpty) // 先过滤空items .map { order order.items.headOption.map(_.price).getOrElse(BigDecimal(0)) // 安全取首项 }某物流系统采用“管道降级”策略当实时分组聚合超时自动回退到缓存的昨日统计保证报表页面永不空白。降级逻辑就插在groupBy之后val grouped orders.groupBy(_.warehouseId) val aggregated Try { grouped.mapValues(calculateWarehouseMetrics) }.getOrElse { // 降级返回缓存的warehouseMetricsMap cachedMetrics }6.3 性能调优视图View与缓存的黄金平衡长管道的最大敌人是内存爆炸。100万订单经过filter→map→groupBy→mapValues可能生成多个百万级中间集合。解决方案是用.view标记惰性计算并在关键节点.forceval pipeline rawOrders .view // 标记为惰性视图 .filter(isValidOrder) .map(enrichOrder) // 添加用户等级等字段 .filter(_.userLevel 3) // 再次过滤 .groupBy(_.category) .mapValues { orders orders.map(_.revenue).sum } .force // 在此处materialize生成最终Map // 对于超大数据集可分段force val enriched rawOrders.view.filter(isValidOrder).map(enrichOrder).force val grouped enriched.groupBy(_.category).force val result grouped.mapValues(_.map(_.revenue).sum).view的代价是每次访问都重新计算所以要在“计算开销小但访问频繁”的环节用.force。经验法则当某个中间结果会被多次使用如分组后的Map被用于统计和导出必须force当只是单次传递给下一个操作view足够。7. 实战避坑指南那些年我们踩过的函数式深坑理论讲完来点血泪教训。以下是我和团队在三年Scala生产项目中反复踩坑、反复填坑总结的“防坑清单”。7.1 坑位1List的O(n)随机访问别当Array用新手常把List当数组用myList(1000)取第1000个元素。List是链表apply(i)是O(n)操作。10万元素取第5万个平均要遍历5万次节点。某推荐系统因此接口P99延迟飙升至2s。解法需要随机访问 → 用VectorO(log32 n)实际≈O(1)需要追加/前置 →ListO(1)或VectorO(1) amortized需要排序后随机访问 →Vector.sorted别用List.sortedval orders: List[Order] ??? // 来自DB的原始列表 val vectorOrders orders.toVector // 一次性转换O(n) val thirdOrder vectorOrders(2) // O(1)访问7.2 坑位2隐式转换的“幽灵依赖”Ordering隐式参数很优雅但当多个隐式同时存在时编译器会懵implicit val byPrice: Ordering[Product] Ordering.by(_.price) implicit val byName: Ordering[Product] Ordering.by(_.name) val sorted products.sorted // ❌ 编译错误ambiguous implicit values解法用implicitly[Ordering[Product]]显式指定或用局部隐式覆盖全局products.sorted(Ordering.by(_.price))最佳实践永远在伴生对象里定义隐式避免跨文件冲突object Product { implicit val defaultOrdering: Ordering[Product] Ordering.by(_.price) } // 现在Product的sorted默认按价格无需import7.3 坑位3flatMap的“扁平化幻觉”flatMap常被误认为“mapflatten”但它的真正价值是处理可能为空的计算// ❌ 错误用flatMap模拟if-else val result list.flatMap { x if (x 0) Some(x * 2) else None // 这是Option的flatMap } // ✅ 正确flatMap用于“一对多”映射 val words List(hello, world) val chars words.flatMap(_.toList) // List(h,e,l,l,o,w,o,r,l,d)若想做条件过滤用filter若想做条件映射用collect// 用collect替代if-else的flatMap val doubledPositives list.collect { case x if x 0 x * 2 }collect是偏函数Partial Function只处理匹配的case天然过滤不匹配项语义更精准。7.4 坑位4并发环境下的不可变幻觉“不可变”不等于“线程安全”。val list List(1,2,3)是线程安全的但val counter new AtomicLong()不是。更隐蔽的坑是// ❌ 危险在Actor中共享可变集合 class MyActor extends Actor { var cache Map.empty[String, List[Int]] // mutable.Map! def receive { case Get(key) sender() ! cache.getOrElse(key, Nil) case Put(key, value) cache (key - value) // 竞态条件 } } // ✅ 正确用不可变集合CAS更新 class MyActor extends Actor { import context.dispatcher var cache Map.empty[String, List[Int]] // immutable.Map def receive { case Put(key, value) // 用compareAndSet或Akka的Stash保证原子性 cache cache (key - value) // 创建新Map安全 } }不可变集合的线程安全仅指集合自身不可变。若你用var引用它cache cache ...是安全的因为赋值是原子的但若用var引用mutable.Map则绝对不安全。8. 进阶武器库Beyond基础四大操作掌握过滤、排序、分组、聚合只是起点。真正的Scala函数式高手会用更强大的组合子拓展能力边界。8.1 惰性求值LazyList解决无限流与大文件LazyList原Stream是处理无限数据源的利器。比如实时日志分析// 模拟无限日志流 def logLines: LazyList[String] LazyList.continually(scala.util.Random.nextString(10)) // 取前1000行含ERROR的日志不加载全部到内存 val errorLogs logLines .filter(_.contains(ERROR)) .take(1000) .force // 此时才真正读取最多1000个ERROR行 // 对大文件逐行处理不OOM def processBigFile(path: String): LazyList[Report] { scala.io.Source.fromFile(path).getLines().to(LazyList) .map(parseLine) .filter(isValid) .map(generateReport) }LazyList的精髓是只有当你调用head、tail、force时才计算对应元素。某日志平台用此方案处理10GB日志文件内存占用稳定在64MB。8.2 类型类Type Class为任意类型添加聚合能力Ordering是类型类的经典案例。我们可以为自定义类型定义聚合行为// 定义Summable类型类 trait Summable[T] { def zero: T def add(a: T, b: T): T } // 为BigDecimal实现 given Summable[BigDecimal] with { def zero BigDecimal(0) def add(a: BigDecimal, b: BigDecimal) a b } // 通用聚合函数 def sumAll[T](list: List[T])(using summable: Summable[T]): T list.foldLeft(summable.zero)(summable.add) // 使用 val total sumAll(List(BigDecimal(1.1), BigDecimal(2.2))) // 3.3类型类让你能为第三方库类型如Joda-Time的DateTime添加聚合逻辑而无需修改其源码。这是Scala函数式最强大的抽象机制之一。8.3 ZIO风格错误处理用Either替代try-catchEither是函数式错误处理的基石。把可能失败的操作封装为Either[Error, Success]type Result[A] Either[String, A] def parseOrder(json: String): Result[Order] Try { // JSON解析逻辑 mapper.readValue(json,

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

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

免费获取报价 →
↑