已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

创建 Sink

了解如何创建和使用各种用于处理 Stream 的 Sink,包括计数、求和、收集、折叠,以及处理成功与失败。

在 Stream 处理中,Sink 用于消费和处理来自 stream 的元素。本节将探索各种 Sink 构造函数,它们让你可以为特定任务创建 Sink

常用构造函数

Sink.head 只取 stream 的第一个元素,并用 Some 包装它。如果 stream 没有任何元素,则返回 None

示例(获取第一个元素)

import { Stream, Sink, Effect, Option } from "effect"

const nonEmptyStream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(nonEmptyStream, Sink.head())) // => Option.some(1)

const emptyStream = Stream.empty

await Effect.runPromise(Stream.run(emptyStream, Sink.head())) // => Option.none()

last

Sink.last 只取 stream 的最后一个元素,并用 Some 包装它。如果 stream 没有任何元素,则返回 None

示例(获取最后一个元素)

import { Stream, Sink, Effect, Option } from "effect"

const nonEmptyStream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(nonEmptyStream, Sink.last())) // => Option.some(4)

const emptyStream = Stream.empty

await Effect.runPromise(Stream.run(emptyStream, Sink.last())) // => Option.none()

count

Sink.count 会消费 stream 的所有元素,并统计传给它的元素数量。

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(stream, Sink.count)) // => 4

sum

Sink.sum 会消费 stream 的所有元素,并对传入的数值求和。

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(stream, Sink.sum)) // => 10

take

Sink.take 会从 stream 中取出指定数量的值,并以数组的形式返回它们。

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(stream, Sink.take(3))) // => [1, 2, 3]

drain

Sink.drain 会忽略它的输入,实际上就是把它们丢弃。

import { Stream, Console, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4).pipe(Stream.tap(Console.log))

await Effect.runPromise(Stream.run(stream, Sink.drain)) // => undefined

timed

Sink.timed 会执行 stream 并测量其执行时间,返回一个 Duration

import { Stream, Schedule, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4).pipe(
  Stream.schedule(Schedule.spaced("100 millis")),
)

Effect.runPromise(Stream.run(stream, Sink.timed)).then(console.log)
/*
Output:
{ _id: 'Duration', _tag: 'Millis', millis: 408 }
*/

forEach

Sink.forEach 会针对传给它的每个元素执行所提供的 effect 函数。

import { Stream, Console, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(stream, Sink.forEach(Console.log))) // => undefined

从成功与失败创建 Sink

正如你可以定义 stream 来保存或操作数据,你也可以使用 Sink.failSink.succeed 函数创建具有特定成功或失败结果的 Sink

成功的 Sink

下面的示例创建了一个 Sink:它不消费上游源中的任何元素,而是立即以一个指定的数值成功结束:

示例(总是以某个值成功的 Sink)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(stream, Sink.succeed(0))) // => 0

失败的 Sink

在这个示例中,这个 Sink 同样不消费上游源中的任何元素,而是以一个 string 类型的指定错误消息失败:

示例(总是以错误消息失败的 Sink)

import { Stream, Sink, Effect, Exit } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromiseExit(Stream.run(stream, Sink.fail("fail!"))) // => Exit.fail("fail!")

收集

收集所有元素

要把数据流中的所有元素汇总到一个数组里,可以使用 Sink.collect

最终输出会按元素被发出的顺序包含 stream 中的所有元素。

示例(收集 Stream 中的所有元素)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(Stream.run(stream, Sink.collect())) // => [1, 2, 3, 4]

收集指定数量

要把 stream 中固定数量的元素收集到一个数组里,可以使用 Sink.take。这个 Sink 在达到指定上限后就停止收集。

示例(收集有限数量的元素)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4, 5)

await Effect.runPromise(
  Stream.run(
    stream,
    // Collect the first 3 elements into an array
    Sink.take(3),
  ),
) // => [1, 2, 3]

在满足条件时收集

要在元素满足特定条件时从 stream 中收集它们,可以使用 Sink.takeWhile。这个 Sink 会持续收集元素,直到给定的谓词返回 false

示例(收集元素直到条件不再满足)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 0, 4, 0, 6, 7)

await Effect.runPromise(
  Stream.run(
    stream,
    // Collect elements while they are not equal to 0
    Sink.takeWhile((n) => n !== 0),
  ),
) // => [1, 2]

收集到 HashSet

要把 stream 的元素累积到一个原生 Set 中,可以用 Sink.reduce 对它们进行折叠。这样可以确保每个元素在最终集合中只出现一次。

示例(把去重后的元素收集到 HashSet)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 2, 3, 4, 4)

await Effect.runPromise(
  Stream.run(
    stream,
    Sink.reduce(
      () => new Set<number>(),
      (s, n) => s.add(n),
    ),
  ),
) // => new Set([1, 2, 3, 4])

收集到指定大小的 HashSet

如果需要以受控方式把元素收集到有指定最大大小的 Set 中,可以用 Sink.reduceWhile 进行折叠,并在集合达到给定上限时停止。

示例(在限制集合大小的情况下收集去重元素)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 2, 3, 4, 4)

await Effect.runPromise(
  Stream.run(
    stream,
    // Collect unique elements, limiting the set size to 3
    Sink.reduceWhile(
      () => new Set<number>(),
      (s) => s.size < 3,
      (s, n) => s.add(n),
    ),
  ),
) // => new Set([1, 2, 3])

收集到 HashMap

对于更复杂的收集场景,可以用 Sink.reduce 把元素折叠进一个原生 Map<K, A>:用一个 key 函数定义每个元素的分组,再用一个合并函数把具有相同 key 的值合并起来。

示例(在 HashMap 中分组并合并 Stream 元素)

在这个示例中,我们用 (n) => n % 3 确定 map 的 key,用 (a, b) => a + b 合并具有相同 key 的元素:

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 3, 2, 3, 1, 5, 1)

await Effect.runPromise(
  Stream.run(
    stream,
    Sink.reduce(
      () => new Map<number, number>(),
      (m, n) => {
        const key = n % 3 // Key function to group by element value
        return m.set(key, m.has(key) ? m.get(key)! + n : n) // Merge function to sum values with the same key
      },
    ),
  ),
) // => new Map([[1, 3], [0, 6], [2, 7]])

收集到 key 数量受限的 HashMap

要把元素累积到一个 key 数量有上限的原生 Map 中,可以用 Sink.reduceWhile 进行折叠,并在 map 达到指定的 key 上限时停止。这需要一个 key 函数来定义每个元素的分组,以及一个合并函数来合并具有相同 key 的值。

示例(限制 HashMap 中收集的 key 数量)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 3, 2, 3, 1, 5, 1)

await Effect.runPromise(
  Stream.run(
    stream,
    Sink.reduceWhile(
      () => new Map<number, number>(),
      (m) => m.size < 3, // Stop once the map has 3 keys
      (m, n) => {
        const key = n // Key function to group by element value
        return m.set(key, m.has(key) ? m.get(key)! + n : n) // Merge function to sum values with the same key
      },
    ),
  ),
) // => new Map([[1, 1], [3, 3], [2, 2]])

折叠

归约元素

如果你想把 stream 归约成单个累积值——也就是按顺序对每个元素应用一个操作——可以使用 Sink.reduce 函数。

示例(用 Sink.reduce 对 Stream 中的元素求和)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4)

await Effect.runPromise(
  Stream.run(
    stream,
    // Use reduce to sequentially add each element, starting with 0
    Sink.reduce(
      () => 0,
      (a, b) => a + b,
    ),
  ),
) // => 10

带终止条件的折叠

有时,你可能想折叠 stream 中的元素,但在满足某个特定条件时就停止这一过程。这被称为“短路”(short-circuiting)。你可以用 Sink.fold 函数做到这一点,它允许你定义终止条件。

示例(带提前停止条件的折叠)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.iterate(0, (n) => n + 1)

await Effect.runPromise(
  Stream.run(
    stream,
    Sink.fold(
      () => 0, // Initial value
      (sum) => sum <= 10, // Termination condition
      (a, b) => Effect.succeed(a + b), // Folding operation
    ),
  ),
) // => 15

折叠到某个上限

要累积元素直到达到特定数量,可以使用 Sink.foldUntil。这个 Sink 会一直折叠元素,直到达到指定上限,然后停止。

示例(累积固定数量的元素)

import { Stream, Sink, Effect } from "effect"

const stream = Stream.make(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

await Effect.runPromise(
  Stream.run(
    stream,
    // Fold elements, stopping after accumulating 3 values
    Sink.foldUntil(
      () => 0,
      3,
      (a, b) => Effect.succeed(a + b),
    ),
  ),
) // => 6

带权重元素的折叠

在某些场景中,你可能希望按定义好的“权重”(weight)或“代价”(cost)来折叠元素,累积元素直到达到指定的最大代价。为此,你可以基于 Sink.fold 构建一个自定义的 Sink,把它的终止条件改为检查累积代价,而不是简单地检查元素数量。

示例(按权重累积元素)

在下面的示例中,每个元素的权重都是 1,当累积权重达到 3 时折叠就会重新开始。

import { Stream, Sink, Effect } from "effect"

const foldWeighted = <A>(cost: (a: A) => number, maxCost: number) =>
  Sink.fold<{ readonly elements: Array<A>; readonly cost: number }, A>(
    () => ({ elements: [], cost: 0 }),
    (state) => state.cost < maxCost, // Keep accumulating while under the max cost
    (state, a) =>
      Effect.succeed({
        elements: [...state.elements, a],
        cost: state.cost + cost(a),
      }),
  ).pipe(Sink.map((state) => state.elements))

const stream = Stream.make(3, 2, 4, 1, 5, 6, 2, 1, 3, 5, 6).pipe(
  Stream.transduce(
    foldWeighted(
      () => 1, // Each element has a weight of 1
      3, // Maximum accumulated cost
    ),
  ),
)

await Effect.runPromise(Stream.runCollect(stream)) // => [[3, 2, 4], [1, 5, 6], [2, 1, 3], [5, 6]]