stream()
- コア
- オプショナル
- ツール
説明
Streamはリアクティブなデータ構造です。スプレッドシートのセルと似ています。
例えば、スプレッドシートではA1 = B1 + C1
とセルに入力し、B1
かC1
の値を変更すると、自動的にA1
の値も変更されます。
同様に、他のStreamに依存するStreamを作ることで、変更した値が自動的に伝播するようになります。非常に時間のかかる計算処理があったときに、再描画ごとではなく、必要なときだけ再計算が行われるようにしたいときに便利です。
StreamはMithrilのコアのディストリビューションにはバンドルされていません。Streamモジュールをインクルードするためには次のようにします:
var Stream = require("mithril/stream")
バンドルのツールチェーンがサポートされない環境であれば、直接モジュールをダウンロードすることもできます。
<script src="https://unpkg.com/mithril-stream"></script>
<script>
タグを使って直接ダウンロードすると、streamライブラリはwindow.m.stream
という名前で利用できます。メインのMithrilスクリプトを使用していてwindow.m
がすでに定義されているのであれば、すでにあるオブジェクトに追加します。そうでなければ新たにwindow.m
が作成されます。もし、Mithrilもscriptタグで直接読み込むのであれば、mithril
がmithril-stream
が追加したwindow.m
オブジェクトを上書きしてしまうため、Mithrilをmithril-stream
よりも先に読み込まなければなりません。この問題は、require(...)
を使ったCommonJSモジュールを利用して使う分には気にする必要はありません。
シグニチャ
Steamの作成
stream = Stream(value)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
value |
any |
No | もし引数が渡されたら、それをStreamの値としてセットします。 |
返り値 | Stream |
Streamを返します。 |
静的メンバー
Stream.combine
上流のStreamが更新されたら計算が行われるStreamを作成します。Streamの結合を参照してください
stream = Stream.combine(combiner, streams)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
combiner |
(Stream..., Array) -> any |
Yes | combiner引数を見てください |
streams |
Array<Stream> |
Yes | 結合するStreamのリストです |
返り値 | Stream |
Streamを返します。 |
combiner
計算されたストリームの値をどのように生成するかを指定します。Streamの結合を参照してください
any = combiner(streams..., changed)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
streams... |
Stream の配列をフラットにしたものです |
No | stream.combine の2つ目の引数に渡されるstreamの配列の0個以上の要素をフラットにしたものです |
changed |
Array<Stream> |
Yes | 更新を引き起こした上流のstreamのリストです |
返り値 | any |
計算された値を返します |
Stream.merge
streamの配列から、値の配列を作るstreamです
stream = Stream.merge(streams)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
streams |
Array<Stream> |
Yes | Streamのリスト |
返り値 | Stream |
入力のstreamの値の配列を値とするstreamを返します |
Stream.scan
全てのstreamの値に対して関数を呼び出し、入力ストリームのすべての値とアキュームレータを一緒に関数に渡して、その結果を値を持つ新しいstreamを返します。
stream = Stream.scan(fn, accumulator, stream)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
fn |
(accumulator, value) -> result |
Yes | アキュームレータと値を受け取り、新しいアキュームレータ値を返します。 |
accumulator |
any |
Yes | アキュームレータの初期値です |
stream |
Stream |
Yes | 値を含むstream |
返り値 | Stream |
結果を含む新しいstream |
Stream.scanMerge
streamとscan関数のペアを要素に持つ配列を受け取り、与えられた関数を使ってこれらのstreamをマージして1つのstreamにします。
stream = Stream.scanMerge(pairs, accumulator)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
pairs |
Array<[Stream, (accumulator, value) -> value]> |
Yes | streamとscan関数のタプルの配列 |
accumulator |
any |
Yes | アキュームレータの初期値です |
返り値 | Stream |
結果を含む新しいstream |
Stream.HALT
下流streamの実行を停止するためにstreamのコールバックを返すことができる特別な値です
Stream["fantasy-land/of"]
このメソッドは機能的にstream
と同じです。Fantasy Landの適用仕様に適合させるために存在しています。詳しくはFantasy Landとは何かのセクションで紹介します。
stream = stream["fantasy-land/of"](value)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
value |
any |
No | もし引数が渡されたら、それをStreamの値としてセットします。 |
返り値 | Stream |
Streamを返します。 |
インスタンスメンバー
stream.map
コールバック関数の結果を値として持つ依存streamを作成します。このメソッドはstream["fantasy-land/map"]のエイリアスです。
dependentStream = stream().map(callback)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
callback |
any -> any |
Yes | このコールバック関数の結果値がstreamの値になります。 |
返り値 | Stream |
Streamを返します。 |
stream.end
trueに設定すると、従属streamの登録を解除する相互依存streamです。終了状態を見てください。
endStream = stream().end
stream["fantasy-land/of"]
このメソッドは機能的にstream
と同じです。Fantasy Landの適用仕様に適合させるために存在しています。詳しくはFantasy Landとは何かのセクションで紹介します。
stream = stream()["fantasy-land/of"](value)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
value |
any |
No | もし引数が渡されたら、それをStreamの値としてセットします。 |
返り値 | Stream |
Streamを返します。 |
stream["fantasy-land/map"]
コールバック関数の結果を値として持つ依存streamを作成します。streamの結合を参照してください。
Fantasy Landの適用仕様に適合させるために存在しています。詳しくはFantasy Landとは何かのセクションで紹介します。
dependentStream = stream()["fantasy-land/of"](callback)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
callback |
any -> any |
Yes | このコールバック関数の結果値がstreamの値になります。 |
返り値 | Stream |
Streamを返します。 |
stream["fantasy-land/ap"]
このメソッドはapply
の短縮形です。もしstreama
が関数を値として持つ時に、他のstreamb
はb.ap(a)
とすることでそれを引数として使用できます。ap
を呼び出すと、streamb
の値を引数に渡して関数を呼び出します。そして、その関数の結果を値として持つstreamを返します。Fantasy Landの適用仕様に適合させるために存在しています。詳しくはFantasy Landとは何かのセクションで紹介します。
stream = stream()["fantasy-land/ap"](apply)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
apply |
Stream |
Yes | 関数が値として設定されているstream |
返り値 | Stream |
Streamを返します。 |
基本的な使い方
streamはMithrilのコアのディストリビューションにはバンドルされていません。プロジェクトに追加するにはrequireを使います。
var stream = require("mithril/stream")
Streamを変数として使う
stream()
はstreamを返します。streamの基本特性としては、streamは変数や、ゲッター・セッター・プロパティのように使うことができます。値を保持したり、変更できます。
var username = stream("ジョン")
console.log(username()) // 出力: "ジョン"
username("ジョン・ドー")
console.log(username()) // 出力: "ジョン・ドー"
主な違いはstreamが関数である点です。そのため、他の上位の関数と組み合わせることができます。
var users = stream()
// fetch APIを使ってユーザーをサーバーから取得
fetch("/api/users")
.then(function(response) {return response.json()})
.then(users)
上記の例では、users
streamはリクエストが完了した時に、そのレスポンスのデータに設定されます。
双方向バインディング
streamはm.withAttr
のような他の上位のの関数と一緒に利用できます。
// stream
var user = stream("")
// streamとの双方向バインディング
m("input", {
oninput: m.withAttr("value", user),
value: user()
})
上記の例では、ユーザーがinputに入力を行うと、user
streamの値がinputフィールドの値に設定されます。
算出プロパティ
streamは算出プロパティを実装するのにも便利です:
var title = stream("")
var slug = title.map(function(value) {
return value.toLowerCase().replace(/\W/g, "-")
})
title("Hello world")
console.log(slug()) // 出力: "hello-world"
上記のサンプルでは、slug
は読み込まれたときではなく、title
が更新されたときに算出されます。
もちろん、複数のstreamを元にした算出プロパティを作ることもできます:
var firstName = stream("ジョン")
var lastName = stream("ドー")
var fullName = stream.merge([firstName, lastName]).map(function(values) {
return values.join("・")
})
console.log(fullName()) // 出力: "ジョン・ドー"
firstName("メアリー")
console.log(fullName()) // 出力: "メアリー・ドー"
Mithrilの算出プロパティは動的に更新されます。streamは複数のstreamに依存しており、値の更新ごとに1度以上呼ばれることはありません。算出プロパティの依存グラフがとても複雑であっても問題ありません。
Streamのチェーン
map
メソッドを使うとことでstreamをチェーンさせることができます。チェーンしたstreamは依存streamとなります。
// 親stream
var value = stream(1)
// 依存stream
var doubled = value.map(function(value) {
return value * 2
})
console.log(doubled()) // 出力: 2
依存streamはリアクティブです。親のstreamの値が更新されると、即座に依存streamの値も更新されます。依存streamが作成されるのが、親のstreamに値をセットする前でも後でも正しく動作します。
特別な値stream.HALT
を返すことで、従属streamの更新を止めることができます:
var halted = stream(1).map(function(value) {
return stream.HALT
})
halted.map(function() {
// 実行されない
})
Streamの結合
streamは複数の親streamに依存させることができます。このような種類のstreamは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()) // 出力: "hello world"
低レベルのメソッドであるstream.combine()
を使うと、各streamを直接扱う事ができるため、より高度なリアクティブな計算処理に使えます。
var a = stream(5)
var b = stream(7)
var added = stream.combine(function(a, b) {
return a() + b()
}, [a, b])
console.log(added()) // 出力: 12
streamは任意の数のstreamに依存することができ、自動更新されることが保証されます。例えば、stream AがBとCから使われていて、DがBとCに依存しているのであれば、stream Aの値が変更された時に一度だけDも更新されます。また、Bだけが新しく、Cがまだ更新されていないといった安定された状態でDが呼ばれることもありません。自動で更新されるのは、不要な下流streamの再計算を避けるので、パフォーマンス上のメリットもあります。
特別な値stream.HALT
を返すことで、従属streamの更新を止めることができます:
var halted = stream.combine(function(stream) {
return stream.HALT
}, [stream(1)])
halted.map(function() {
// 実行されない
})
Streamの状態
streamには、次の3つの状態があります: ペンディング, アクティブ, 終了
ペンディング状態
ペンディング状態は、stream()
で作成されたがまだパラメータが設定されていない状態です。
var pending = stream()
streamが1つ以上のstreamに依存していて、どれか1つでもペンディング状態の親があると、依存streamもペンディング状態になり、値の更新が行われなくなります。
var a = stream(5)
var b = stream() // ペンディング状態の stream
var added = stream.combine(function(a, b) {
return a() + b()
}, [a, b])
console.log(added()) // 出力: undefined
上記の例では、親のb
がペンディングなので、added
もペンディング状態です。
これはstream.map
を使って作ったstreamでも同じ動作になります:
var value = stream()
var doubled = value.map(function(value) {return value * 2})
console.log(doubled()) // `doubled` がペンディングなのでundfinedが出力
アクティブ状態
値を受け取ったstreamはアクティブ(もしくは終了状態)となります。
var stream1 = stream("hello") // stream1 がアクティブ
var stream2 = stream() // stream2 をペンディング状態で作成し、
stream2("world") // 後にアクティブに
複数の親を持つ依存streamの場合、すべての親がアクティブな時にアクティブとなります。
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
もアクティブ化され、streamの値が"hello world"
となります。
終了状態
stream.end(true)
を呼ぶと、依存streamに影響を与えることがなくなります。これを使うと、親streamと依存streamの接続を効率的に削除することができます。
var value = stream()
var doubled = value.map(function(value) {return value * 2})
value.end(true) // 終了状態の設定
value(5)
console.log(doubled())
// `doubled` はもう `value` に依存してないので、undefined出力
終了したstreamもまだ状態のコンテナとしては機能します。そのため、終了状態に設定しても、ゲッター・セッターとしては使えます。
var value = stream(1)
value.end(true) // 終了状態にセット
console.log(value(1)) // 出力: 1
value(2)
console.log(value()) // 出力: 2
終了したstreamは、ライフサイクルが決まっているケースで便利です。例えば、DOMエレメントをドラッグしている間だけmousemove
イベントに対してリアクティブに動作し、ドロップしたら接続を切りたい場合などです。
Streamのシリアライズ
streamは.toJSON()
メソッドを実装しています。JSON.stringify()
にstreamを渡すと、streamが持っている値がシリアライズされます。
var value = stream(123)
var serialized = JSON.stringify(value)
console.log(serialized) // 出力: 123
streamはvalueOf
メソッドもも実装しているため、streamの値を統一インタフェースで扱うことができます。
var value = stream(123)
console.log("test " + value) // 出力 "test 123"
Streamはレンダリングを起動しない
Knockoutなどのようなライブラリと異なり、Mithrilのstreamはテンプレートの再描画をトリガーしません。再描画はMithrilのコンポーネントのビューで定義されたイベントハンドラ、ラウトの変更、m.request
の呼び出しが解決したタイミングで行われます。
setTimeout
/setInterval
, websocketのサブスクリプション、サードパーティーライブラリのイベントハンドラなど、他の非同期イベントで再描画を行う場合は、手動でm.redraw()
を呼びます。
Fantasy Landとは何か
Fantasy Landは一般的な代数構造の相互運用性のための共通のインタフェースを定義したものです。Fantasy Landの仕様に準拠したライブラリは、これらのライブラリがどのような内部構造で実装されているかに関係なく、汎用的な関数型スタイルのコードから利用できます。
例えば、汎用関数plusOne
を作りたいとします。JavaScriptのネイティブ実装では次のようになるでしょう:
function plusOne(a) {
return a + 1
}
この実装の問題点は、数値型に対してのみしか利用できない点です。しかし、a
の値によっては、このロジックはエラー状態を発生する可能性があります。エラー状態は、SanctuaryやRamda-FantasyではMaybeやEitherとしてラップされますし、Mithrilのstreamやflydのstreamなどでも発生する可能性があります。理想的には、a
が持つ可能性のあるすべての型に対して、ラップ/アンラップ/エラーハンドリングコードなどを書きたくはないでしょう。
Fantasy Landはこの分野の手助けをしてくれます。Fantasy Landの代数のルールに従って関数を書いてみましょう:
var fl = require("fantasy-land")
function plusOne(a) {
return a[fl.map](function(value) {return value + 1})
}
このメソッドは、Fantasy Land準拠のFunctor(such as R.Maybe
, S.Either
, stream
)に対して動作します。
この例だけを見ると、単に複雑にしているだけに見えるかもしれませんが、複雑さとトレードオフのメリットがあります。シンプルなシステムで数字を増やすだけの単純なplusOne
実装はこのケースでは理にかなっていますが、Fantasy Landの実装にすると、数多くの多くの抽象化された再利用可能なアルゴリズムのラッパーが利用できます。
Fantasy Landに対応すべきかどうかを判断するには、チームが関数型プログラミングに精通しているか、新機能の作成と締め切りのプレッシャーに負けずにコード品質を維持できるほど教育されているかどうかを確認する必要があります。関数型スタイルのプログラミングは、数多くの小さなよく定義された関数のコンパイル、キュレーション、マスタリングに大きく依存します。したがって、実践的なドキュメントが書けないチームや、関数指向の言語の経験が不足しているチームには適していません。
License: MIT. © Leo Horie.