
面向 Dart 的函数式编程,
惰性与并发开箱即用。
FxDart 是 FxTS 的 Dart 移植版本 —— 一个用于在同步与异步数据上组合惰性管道的库, 把六个各耗时 1 秒的串行请求变成 2 秒完成的并发批处理, 只需一次方法调用。
📒 看看实际效果:Daily Ledger —— 用 FxDart 构建的完整应用 →
⚖️ Dart vs FxDart —— 53 个真实任务,并排对比 →
⚡ RxDart vs FxDart —— push 对 pull,50 条诚实判定 →
▲ 这是可运行的 —— 修改代码并按下 运行。 它会用真正的 Dart 编译器编译,并在你的浏览器中执行。
FxDart 是什么?
FxDart 把 FxTS 的编程模型带到了 Dart:一套约 120 个小而可组合的函数, 用于变换集合与异步数据。它由三个核心理念定义:
- 惰性求值 ——
map、filter、take这类操作符只构建管道,在终结操作符 (toList、each、reduce……) 拉取数据之前不做任何工作。对一个百万元素的范围调用.take(3),只会计算 3 个结果。 - 同步与异步共用一套模型 —— 同样的操作符名称既适用于普通的
Iterable,也适用于FxAsyncIterable, 即 FxDart 基于拉取的异步序列(并提供双向的Stream桥接)。 - 声明式并发 ——
concurrent(n)要求上游管道每次求值n个元素, 同时保持结果有序。这是 FxTS 的标志性特性, 已忠实移植到 Dart。
随时间到来的事件,以及作为值的失败,在另外两面上。 选哪一面? 才是 判断;本页仍是介绍页。
为什么需要它?
Dart 已经有 Iterable 和 Stream。FxDart 的价值,
体现在它们力所不及的地方:
| 问题 | 原生 Dart | FxDart |
|---|---|---|
| 把并发 API 调用限制为 n 个,并保持顺序 | 手写队列、Completer,以及繁琐的状态管理 |
.map(fetch).concurrent(3) |
| 异步变换管道 | Stream 是基于推送的;把 await、背压和惰性混在一起会变得复杂 |
基于拉取的链式调用 —— 每个值只在被请求时才计算 |
| 数据整理(分组、索引、计数、分区、zip、分块……) | 每次都要手写循环 | 每个概念对应一个经过充分测试的具名函数 |
| 可读的多步变换 | 嵌套调用或中间变量 | 从左到右的 fx() 链式调用,类型完备 |
优点 & 缺点
✓ 优点
- 免费获得惰性 —— 管道会短路;只计算被请求的值。
- 保序的并发,一个操作符即可实现:
concurrent(n)/ 按完成顺序输出的concurrentPool(n)。 - 类型完备的链式调用 ——
fx()让类型推断贯穿始终;同步操作符就是作用于原生Iterable的普通函数,因此一切都能与常规 Dart 代码互操作。 - 小而专注的函数 —— 约 120 个操作符,覆盖变换 / 过滤 / 切片 / 组合 / 聚合 / 对象 / 工具。
- 久经考验的语义 —— 行为从 FxTS 移植而来,同时带来了它 850+ 个测试。
- 零依赖 —— 纯 Dart 实现。
✗ 缺点
- 没有 data-last 柯里化 —— Dart 不支持函数重载,因此 FxTS 柯里化的
pipe风格在这里变成了链式调用;动态的pipe()会丢失静态类型。 - 多了一层异步抽象 ——
FxAsyncIterable之所以存在,是因为Stream无法表达并发的反向通道;桥接很容易,但终究是一个需要额外学习的概念。 - 学习曲线 —— 用惰性管道思考,与写命令式循环并不相同。
- 并非总是最快 —— 对于极小的热点循环,手写
for可能胜过操作符组合;FxDart 的优化目标是清晰度与 I/O 密集型场景。 - 部分 TS API 无法逐字移植 —— 它们改用了更符合 Dart 习惯的写法:
curry变成了带类型的.curried扩展 getter,旧名称则作为已废弃的桩函数保留,便于迁移。
并发初体验
六个各耗时 300 ms 的模拟请求 —— 串行约需 1.8 s,使用
concurrent(3) 约需 0.6 s。试着改改这个数字: