Mithril 1.1.0

stream()


説明

Streamはリアクティブなデータ構造です。スプレッドシートのセルと似ています。

例えば、スプレッドシートではA1 = B1 + C1とセルに入力し、B1C1の値を変更すると、自動的に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タグで直接読み込むのであれば、mithrilmithril-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が関数を値として持つ時に、他のstreambb.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)

上記の例では、usersstreamはリクエストが完了した時に、そのレスポンスのデータに設定されます。

双方向バインディング

streamはm.withAttrのような他の上位のの関数と一緒に利用できます。

// stream
var user = stream("")

// streamとの双方向バインディング
m("input", {
    oninput: m.withAttr("value", user),
    value: user()
})

上記の例では、ユーザーがinputに入力を行うと、userstreamの値が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の値によっては、このロジックはエラー状態を発生する可能性があります。エラー状態は、SanctuaryRamda-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.