コンテンツにスキップ

scan — 時間軸に沿った状態の進化

scanは、このライブラリで最も強力なプリミティブの一つです。これは、時間とともに進化する「状態」を持つタイムラインを構築するための、基本的なツールとなります。

畳み込みの3つの次元:構造と時間

Section titled “畳み込みの3つの次元:構造と時間”

scanのユニークな役割を理解するため、プログラミングにおける3種類の「畳み込み(fold)」を比較します。最初の2つは構造 (Structure) を扱い、3つ目のscan時間 (Time) という全く異なる次元を扱います。

  1. fold (構造の畳み込み): [1, 2, 3] のような配列(静的なコレクション構造)を扱い、一つの最終的な値(例:6)を算出します。
  2. foldTimelines (構造の畳み込み): [Timeline<1>, Timeline<2>, Timeline<3>] のようなタイムラインのリスト(動的なコレクション構造)を扱い、それらを一つの結果タイムラインに合成します。
  3. scan (時間の畳み込み): 1 -> 2 -> 3 のような単一のタイムライン(時間的なイベントストリーム)を扱い、イベントが発生するたびに途中経過の値を保持する新しいタイムライン(例:1 -> 3 -> 6)を生成します。

これらの違いをまとめたのが、以下の比較表です。

特徴fold (構造)foldTimelines (構造)scan (時間)
目的最終的な集計構造的な合成時間的な集約
入力配列などタイムラインのリスト1つのタイムライン
処理一度に全体一度に全体イベントのたびに
出力1つの最終値最終結果のタイムライン途中経過のタイムライン
比喩買い物かごの合計金額 🧾複数の投票の最終集計 🗳️銀行口座の残高推移 📈

foldfoldTimelinesは、共に入力となる「コレクション構造」を一つにまとめる点で非常によく似ています。一方でscanは、単一のストリームの時間的な変化を追跡するための、全く異なる目的を持つツールであることが、この比較から明確になります。それは過去の履歴を記憶し、新しい入力に基づいて状態を更新していく、状態を持つタイムラインを構築するための核心的な操作なのです。


F#: scan: ('state -> 'input -> 'state) -> 'state -> Timeline<'input> -> Timeline<'state>
Section titled “F#: scan: ('state -> 'input -> 'state) -> 'state -> Timeline<'input> -> Timeline<'state>”

Note: In F#, scan is a standalone function.

TS: .scan<S>(accumulator: (acc: S, value: T) => S, seed: S): Timeline<S>
Section titled “TS: .scan<S>(accumulator: (acc: S, value: T) => S, seed: S): Timeline<S>”

scanの動作を、流れてくる数値を合計していくカウンターで見てみましょう。

// 数値のタイムラインを作成
const numberStream = Timeline<number>(0);
// scanを使って、流れてくる数値を合計していく
const runningTotal = numberStream.scan(
(sum, currentValue) => sum + currentValue, // accumulator: 現在の合計値に新しい値を加算
0 // seed: 合計値の初期値
);
// runningTotalは常に最新の合計値を保持する
console.log(runningTotal.at(Now)); // 0
numberStream.define(Now, 5);
console.log(runningTotal.at(Now)); // 5 (0 + 5)
numberStream.define(Now, 10);
console.log(runningTotal.at(Now)); // 15 (5 + 10)
numberStream.define(Now, -3);
console.log(runningTotal.at(Now)); // 12 (15 - 3)

入力タイムラインに数値が流れるたびに、scanによって構築された状態タイムライン(合計値)がリアルタイムに更新されていく様子を視覚化するデモ。