stream()
説明
Stream(ストリーム)は、スプレッドシートアプリケーションのセルと同様に、リアクティブなデータ構造です。
例えば、スプレッドシートにおいて A1 = B1 + C1
である場合、B1
または C1
の値を変更すると、A1
の値も自動的に変更されます。
同様に、あるストリームを別のストリームに依存させることで、一方の値を変更すると、もう一方が自動的に更新されるようにすることができます。これは、計算コストが高く、例えば再描画のたびに実行するのではなく、必要な場合にのみ実行したい場合に便利です。
ストリームは、Mithril.js のコアディストリビューションには含まれていません。ストリームモジュールを利用するには、以下のように記述します。
var Stream = require('mithril/stream');
バンドルツールチェーンが利用できない環境では、モジュールを直接ダウンロードすることもできます。
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
<script>
タグで直接ロードした場合(require
ではなく)、ストリームライブラリは window.m.stream
として利用可能になります。window.m
が既に定義されている場合(例えば、メインの Mithril.js スクリプトも使用している場合)、既存のオブジェクトにアタッチされます。それ以外の場合は、新しい window.m
が作成されます。ストリームを Mithril.js と組み合わせて生のスクリプトタグとして使用する場合は、mithril/stream
によって定義された window.m
オブジェクトが mithril
によって上書きされないように、mithril
の前に mithril/stream
をページに含める必要があります。これは、ライブラリが CommonJS モジュールとして消費される場合(require(...)
を使用)には問題になりません。
シグネチャ
ストリームを生成します。
stream = Stream(value)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
value | any | いいえ | この引数が存在する場合、ストリームの初期値として設定されます。 |
戻り値 | Stream | ストリームを返します。 |
静的メンバー
Stream.combine
依存するストリームのいずれかが更新されるとリアクティブに更新される算出ストリームを生成します。ストリームの結合を参照してください。
stream = Stream.combine(combiner, streams)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
combiner | (Stream..., Array) -> any | はい | combiner引数を参照。 |
streams | Array<Stream> | はい | 結合するストリームのリスト |
戻り値 | Stream | ストリームを返します。 |
combiner
計算されたストリームの値を生成する方法を指定します。ストリームの結合を参照してください。
any = combiner(streams..., changed)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
streams... | Streams のスプレッド構文 | いいえ | stream.combine の 2 番目の引数として渡されたストリームに対応する、ゼロ個以上のストリームのスプレッド構文 |
changed | Array<Stream> | はい | 更新によって影響を受けたストリームのリスト |
戻り値 | any | 計算された値を返します。 |
Stream.merge
複数のストリームの値を配列として持つストリームを生成します。
stream = Stream.merge(streams)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
streams | Array<Stream> | はい | ストリームのリスト |
戻り値 | Stream | 入力ストリームの値の配列を値とするストリームを返します。 |
Stream.scan
アキュムレータと入力値を使って関数を適用した結果を持つ新しいストリームを生成します。
アキュムレーター関数内で特別な値 stream.SKIP
を返すことで、依存ストリームが更新されないようにすることができます。
stream = Stream.scan(fn, accumulator, stream)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
fn | (accumulator, value) -> result |SKIP | はい | アキュムレーターと値の引数を取り、同じ型のアキュムレーターの新しい値を返す関数 |
accumulator | any | はい | アキュムレーターの初期値 |
stream | Stream | はい | 値を含むストリーム |
戻り値 | Stream | 結果を含む新しいストリームを返します。 |
Stream.scanMerge
ストリームとスキャン関数のペア配列を入力として受け取り、指定された関数を使用してそれらのすべてのストリームを単一のストリームにマージします。
stream = Stream.scanMerge(pairs, accumulator)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | はい | ストリームとスキャン関数のタプルの配列 |
accumulator | any | はい | アキュムレーターの初期値 |
戻り値 | Stream | 結果を含む新しいストリームを返します。 |
Stream.lift
依存するストリームのいずれかが更新されるとリアクティブに更新される計算されたストリームを生成します。ストリームの結合を参照してください。combine
とは異なり、入力ストリームは可変数の引数(配列の代わりに)であり、コールバックはストリームではなくストリームの値を受け取ります。changed
パラメーターはありません。これは一般的に combine
より使いやすい関数です。
stream = Stream.lift(lifter, stream1, stream2, ...)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
lifter | (any...) -> any | はい | lifter 引数を参照してください。 |
streams... | Streams のリスト | はい | リフトするストリーム |
戻り値 | Stream | ストリームを返します。 |
lifter
計算されたストリームの値を生成する方法を指定します。ストリームの結合を参照してください。
any = lifter(streams...)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
streams... | Streams のスプレッド構文 | いいえ | stream.lift に渡されたストリームの値に対応する、ゼロ個以上の値のスプレッド構文 |
戻り値 | any | 計算された値を返します。 |
Stream.SKIP
下流の処理をスキップさせるためにストリームコールバックに返すことができる特別な値
Stream["fantasy-land/of"]
このメソッドは、機能的には stream
と同じです。これは、Fantasy Land の Applicative 仕様に準拠するために存在します。詳細については、Fantasy Land とはセクションを参照してください。
stream = Stream["fantasy-land/of"](value)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
value | any | いいえ | この引数が存在する場合、ストリームの初期値として設定されます。 |
戻り値 | Stream | ストリームを返します。 |
インスタンスメンバー
stream.map
コールバック関数の結果を値とする依存ストリームを生成します。このメソッドは、stream["fantasy-land/map"] のエイリアスです。
dependentStream = stream().map(callback)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
callback | any -> any | はい | 戻り値がストリームの値になるコールバック |
戻り値 | Stream | ストリームを返します。 |
stream.end
true
に設定すると依存ストリームの登録を解除する共依存ストリーム。終了状態を参照してください。
endStream = stream().end
stream["fantasy-land/of"]
このメソッドは、機能的には stream
と同じです。これは、Fantasy Land の Applicative 仕様に準拠するために存在します。詳細については、Fantasy Land とはセクションを参照してください。
stream = stream()["fantasy-land/of"](value)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
value | any | いいえ | この引数が存在する場合、ストリームの初期値として設定されます。 |
戻り値 | Stream | ストリームを返します。 |
stream["fantasy-land/map"]
コールバック関数の結果を値とする依存ストリームを生成します。ストリームのチェーンを参照してください。
このメソッドは、Fantasy Land の Applicative 仕様に準拠するために存在します。詳細については、Fantasy Land とはセクションを参照してください。
dependentStream = stream()["fantasy-land/map"](callback)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
callback | any -> any | はい | 戻り値がストリームの値になるコールバック |
戻り値 | Stream | ストリームを返します。 |
stream["fantasy-land/ap"]
このメソッドの名前は apply
の略です。ストリーム a
の値が関数の場合、別のストリーム b
はそれを b.ap(a)
の引数として使用できます。ap
を呼び出すと、ストリーム b
の値を引数として関数が呼び出され、関数呼び出しの結果が値である別のストリームが返されます。このメソッドは、Fantasy Land の Applicative 仕様に準拠するために存在します。詳細については、Fantasy Land とはセクションを参照してください。
stream = stream()["fantasy-land/ap"](apply)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
apply | Stream | はい | 値が関数であるストリーム |
戻り値 | Stream | ストリームを返します。 |
基本的な使い方
ストリームは、コアの Mithril.js ディストリビューションには含まれていません。プロジェクトに含めるには、そのモジュールを require
します。
var stream = require('mithril/stream');
変数としてのストリーム
stream()
はストリームを返します。最も基本的なレベルでは、ストリームは変数または getter-setter プロパティと同様に機能します。状態を保持し、変更することができます。
var username = stream('John');
console.log(username()); // logs "John"
username('John Doe');
console.log(username()); // logs "John Doe"
主な違いは、ストリームが関数であるため、高階関数と組み合わせることができる点です。
var users = stream();
// fetch API を使用してサーバーからユーザーをリクエストする
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
上記の例では、リクエストが解決されると、users
ストリームに応答データが設定されます。
双方向バインディング
ストリームは、イベントコールバックなどからも入力できます。
// ストリーム
var user = stream('');
// ストリームへの双方向バインディング
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
上記の例では、ユーザーが入力すると、user
ストリームが入力フィールドの値で更新されます。
計算されたプロパティ
ストリームは、計算プロパティを実装するのに役立ちます。
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // logs "hello-world"
上記の例では、slug
の値は、slug
が読み取られるときではなく、title
が更新されるときに計算されます。
もちろん、複数のストリームに基づいてプロパティを計算することも可能です。
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // logs "John Doe"
firstName('Mary');
console.log(fullName()); // logs "Mary Doe"
Mithril.js の計算されたプロパティは、アトミックに更新されます。複数のストリームに依存するストリームは、計算されたプロパティの依存関係グラフがどれほど複雑であっても、値の更新ごとに複数回呼び出されることはありません。
ストリームのチェーン
ストリームは、map
メソッドを使用してチェーンすることができます。チェーンされたストリームは、依存ストリーム とも呼ばれます。
// 親ストリーム
var value = stream(1);
// 依存ストリーム
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // logs 2
依存ストリームは リアクティブ です。親ストリームの値が更新されると、その値も更新されます。これは、依存ストリームが親ストリームの値の設定前、後どちらに作成されたかに関わらず発生します。
特別な値 stream.SKIP
を返すことで、依存ストリームが更新されないようにすることができます。
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// 実行されない
});
ストリームの結合
ストリームは、複数の親ストリームに依存することができます。これらの種類のストリームは、stream.merge()
を介して作成できます。
var a = stream('hello');
var b = stream('world');
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
console.log(greeting()); // logs "hello world"
または、ヘルパー関数 stream.lift()
を使用することもできます。
var a = stream('hello');
var b = stream('world');
var greeting = stream.lift(
function (_a, _b) {
return _a + ' ' + _b;
},
a,
b
);
console.log(greeting()); // logs "hello world"
より高度なユースケースに対応するため、リアクティブな計算においてストリーム自体を公開する stream.combine()
という低レベルなメソッドも存在します。
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // logs 12
ストリームは無数の依存ストリームを持て、アトミックに更新されることが保証されています。例えば、ストリーム A に 2 つの依存ストリーム B と C があり、4 番目のストリーム D が B と C の両方に依存している場合、A の値が変更された場合、ストリーム D は 1 回だけ更新されます。これにより、ストリーム D のコールバックが、B に新しい値があるが C に古い値がある場合など、不安定な値で呼び出されることはありません。原子性は、ダウンストリームを不必要に再計算しないというパフォーマンス上の利点ももたらします。
特別な値 stream.SKIP
を返すことで、依存ストリームが更新されないようにすることができます。
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// 実行されない
});
ストリームの状態
特定の時点で、ストリームは、保留中、アクティブ、および 終了 の 3 つの状態のいずれかになります。
保留中の状態
保留中のストリームは、引数なしで stream()
を呼び出すことによって作成できます。
var pending = stream();
ストリームが複数のストリームに依存しており、その親ストリームのいずれかが保留中の状態にある場合、依存ストリームも保留中の状態になり、その値を更新しません。
var a = stream(5);
var b = stream(); // 保留中のストリーム
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // logs undefined
上記の例では、added
は保留中のストリームです。これは、その親 b
も保留中であるためです。
これは、stream.map
を介して作成された依存ストリームにも適用されます。
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // `doubled` が保留中のため、undefined をログに記録します
アクティブな状態
ストリームが値を受け取るとアクティブ状態になります(ストリームが終了していない場合)。
var stream1 = stream('hello'); // stream1 はアクティブです
var stream2 = stream(); // stream2 は保留中で開始されます
stream2('world'); // 次にアクティブになります
複数の親を持つ依存ストリームは、そのすべての親がアクティブな場合にアクティブになります。
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
上記の例では、a
ストリームはアクティブですが、b
は保留中です。b("world")
を設定すると、b
がアクティブになり、したがって greeting
もアクティブになり、値 "hello world"
に更新されます。
終了状態
ストリームは、stream.end(true)
を呼び出すことによって、その依存ストリームに影響を与えるのを停止できます。これにより、ストリームとその依存ストリーム間の接続が効果的に削除されます。
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // 終了状態に設定
value(5);
console.log(doubled());
// `doubled` が `value` に依存しなくなったため、undefined をログに記録します
ストリームは依然として状態コンテナとしての性質を保持しています。つまり、終了後でも getter-setter として使用できます。
var value = stream(1);
value.end(true); // 終了状態に設定
console.log(value(1)); // logs 1
value(2);
console.log(value()); // logs 2
ストリームの終了は、ストリームの有効期間が限られている場合(例えば、DOM 要素がドラッグされている間のみ mousemove
イベントに反応し、ドロップされた後は反応しない場合)に役立ちます。
ストリームのシリアル化
ストリームは .toJSON()
メソッドを備えています。ストリームが JSON.stringify()
の引数として渡されると、ストリームの値がシリアル化されます。
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // logs 123
ストリームはレンダリングをトリガーしません
Knockoutなどのライブラリと異なり、Mithril.js ストリームはテンプレートの再レンダリングをトリガーしません。再描画は、Mithril.js コンポーネントビューで定義されたイベントハンドラー、ルートの変更、または m.request
呼び出しの解決に応じて発生します。
他の非同期イベント(例えば、setTimeout
/setInterval
、websocket サブスクリプション、サードパーティライブラリのイベントハンドラーなど)に応じて再描画が必要な場合は、手動で m.redraw()
を呼び出す必要があります。
Fantasy Land とは
Fantasy Land は、一般的な代数的構造の相互運用性を指定します。簡単に言うと、Fantasy Land仕様に準拠したライブラリを使用することで、それらのライブラリがコンストラクトをどのように実装しているかに関わらず機能する、汎用的な関数型スタイルのコードを作成できるということです。
例えば、plusOne
という汎用関数を作成するとします。単純な実装は次のようになります。
function plusOne(a) {
return a + 1;
}
この実装の問題は、数値でのみ使用できることです。ただし、a
の値を生成するロジックが、エラー状態(Sanctuary や Ramda-Fantasy などのライブラリからの Maybe または Either でラップされている)、または Mithril.js ストリーム、Flyd ストリームなどを生成する可能性もあります。理想的には、a
が持つ可能性のあるすべてのタイプに対して同じ関数の同様のバージョンを作成したくなく、ラッピング/アンラッピング/エラー処理コードを繰り返し記述したくありません。
ここで Fantasy Land が役立ちます。Fantasy Land 代数に関してその関数を書き直しましょう。
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
これで、このメソッドは、R.Maybe
、S.Either
、stream
など、Fantasy Land 準拠の Functor で動作します。
この例は複雑に見えるかもしれませんが、複雑さのトレードオフです。単純な plusOne
実装は、単純なシステムがあり、数値をインクリメントするだけであれば意味がありますが、Fantasy Land 実装は、多くのラッパー抽象化と再利用されたアルゴリズムを備えた大規模なシステムがある場合に、より強力になります。
Fantasy Land を採用するかどうかを決定する際には、関数型プログラミングに関するチームの知識を考慮し、チームがコード品質を維持するためにコミットできる規律のレベル(新しい機能を作成し、締め切りを守るというプレッシャーに対して)に関して現実的である必要があります。関数型スタイルのプログラミングは、小さく正確に定義された関数の大規模な集合のコンパイル、キュレーション、習得に大きく依存するため、堅牢なドキュメント作成習慣を持たないチームや、関数型言語の経験が不足しているチームには適していません。