frp in practice
DESCRIPTION
TRANSCRIPT
Functional Reactive Programming 実践編~ 画面作成、リクエスト処理 ~
@rf0444
利用ライブラリ
• Bacon.js
• https://github.com/raimohanska/bacon.js
• jQuery
おしながき
• EventStream と Property
•画面を作る
•リクエスト処理
EventStream と Property
EventStream
• 発生するイベントの列 を表す
• クリックされた、など
時間
値
Property
• 時間によって変化する値 を表す
• View に表示する値、最後に返ってきたレスポンス など
時間
値
EventStream / Property
• EventStream#merge(EventStream)
• 2つの EventStream をくっつけた EventStream を作る。 時間
値
時間
値
時間
値
e1
e2
e1.merge(e2)
EventStream / Property
• EventStream#toProperty([initVal])
• EventStream に 値が流れてくるタイミングで、値が変化する Property を作る。
• 引数に初期値を指定できる。(なしも可)
時間
値
時間
値
v0
es
es.toProperty(v0)
EventStream / Property
• Property#changes()
• Property の値が変化したタイミングで、変化後の値が流れる EventStream を作る。
• 初期値は流れない
時間
値 p.changes()
時間
値 p
EventStream / Property
• Property#sampledBy(EventStream)
• EventStream に値が流れた時点の Property の値が流れる EventStream を作る。
時間
値
時間
値
時間
値
p
es
p.sampledBy(es)
画面を作る
設計方針
• 出来るだけ副作用を排除したい。
• Callback 内処理を、単純な副作用だけにしたい。
例: Click Counter
クリックすると増える
例: Click Counter
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />').text(conf.text); return { el: el, streams: { clicked: el.asEventStream('click') } }; }; var mkText = function(conf) { var el = $('<span />'); conf.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ text: 'click' }); var text = mkText({ text: button.streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }); $('body').append(button.el).append(' ').append(text.el);});
例: Click Counter
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />').text(conf.text); return { el: el, streams: { clicked: el.asEventStream('click') } }; }; var mkText = function(conf) { var el = $('<span />'); conf.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ text: 'click' }); var text = mkText({ text: button.streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }); $('body').append(button.el).append(' ').append(text.el);});
ボタン右のテキストの値(時間によって変化する)
例: Click Counter
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />').text(conf.text); return { el: el, streams: { clicked: el.asEventStream('click') } }; }; var mkText = function(conf) { var el = $('<span />'); conf.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ text: 'click' }); var text = mkText({ text: button.streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }); $('body').append(button.el).append(' ').append(text.el);});
クリックされたら 1 が流れてくる EventStream
例: Click Counter
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />').text(conf.text); return { el: el, streams: { clicked: el.asEventStream('click') } }; }; var mkText = function(conf) { var el = $('<span />'); conf.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ text: 'click' }); var text = mkText({ text: button.streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }); $('body').append(button.el).append(' ').append(text.el);});
クリックされたら 1 が流れてくる EventStream
0 から順に、足して畳み込んでいく(初期値 0 の Property ができる)
例: Click Counter
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />').text(conf.text); return { el: el, streams: { clicked: el.asEventStream('click') } }; }; var mkText = function(conf) { var el = $('<span />'); conf.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ text: 'click' }); var text = mkText({ text: button.streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }); $('body').append(button.el).append(' ').append(text.el);});
副作用(値変化時の処理登録、テキストの中身を変更)
副作用(DOM 要素登録)
例: Counting Button
クリックすると増える
例: Counting Button
クリックすると増える
Property を作るために、作成後の button の EventStream が必要
例: Counting Button
クリックすると増える
Property を作るために、作成後の button の EventStream が必要
EventStream -> Property な関数を渡すようにしてみる
例: Counting Button
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />'); var streams = { clicked: el.asEventStream('click') }; var properties = conf.f(streams); properties.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ f: function(streams) { return { text: streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }; }, }); $('body').append(button.el);});
例: Counting Button
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />'); var streams = { clicked: el.asEventStream('click') }; var properties = conf.f(streams); properties.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ f: function(streams) { return { text: streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }; }, }); $('body').append(button.el);});
EventStream -> Property な関数
例: Counting Button
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />'); var streams = { clicked: el.asEventStream('click') }; var properties = conf.f(streams); properties.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ f: function(streams) { return { text: streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }; }, }); $('body').append(button.el);});
渡された EventStream を畳み込んで、Propertyを作る
例: Counting Button
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />'); var streams = { clicked: el.asEventStream('click') }; var properties = conf.f(streams); properties.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ f: function(streams) { return { text: streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }; }, }); $('body').append(button.el);});
先に EventStream を作っておいて、
例: Counting Button
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />'); var streams = { clicked: el.asEventStream('click') }; var properties = conf.f(streams); properties.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ f: function(streams) { return { text: streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }; }, }); $('body').append(button.el);});
渡された関数に適用して、Property を得る
先に EventStream を作っておいて、
$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = function(conf) { var el = $('<button />'); var streams = { clicked: el.asEventStream('click') }; var properties = conf.f(streams); properties.text.assign(function(text) { el.text(text); }); return { el: el }; }; var button = mkButton({ f: function(streams) { return { text: streams.clicked .map(constant(1)) .scan(0, function(a, b) { return a + b; }), }; }, }); $('body').append(button.el);});
例: Counting Button
副作用(値変化時の処理登録、テキストの中身を変更)
副作用(DOM 要素登録)
例: Cross Counting Button
クリックすると 反対側が増える
例: Cross Counting Button
クリックすると 反対側が増える
Property を作るために、別の button の EventStream が必要
(相互に要求)
例: Cross Counting Button
クリックすると 反対側が増える
Property を作るために、別の button の EventStream が必要
(相互に要求)
Bus を使い、一旦 EventStream として渡しておき、
button を作るときに Bus につなぐ
例: Cross Counting Button$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; }, }; var streams1 = mkButton.streams(); var streams2 = mkButton.streams(); var logic = function(s) { return s.clicked.map(constant(1)).scan(0, function(a, b) { return a + b; }); }; var button1 = mkButton.create({ properties: { text: logic(streams2) }, streams: streams1, }); var button2 = mkButton.create({ properties: { text: logic(streams1) }, streams: streams2, }); $('body').append(button1.el).append(' ').append(button2.el);});
例: Cross Counting Button$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; }, }; var streams1 = mkButton.streams(); var streams2 = mkButton.streams(); var logic = function(s) { return s.clicked.map(constant(1)).scan(0, function(a, b) { return a + b; }); }; var button1 = mkButton.create({ properties: { text: logic(streams2) }, streams: streams1, }); var button2 = mkButton.create({ properties: { text: logic(streams1) }, streams: streams2, }); $('body').append(button1.el).append(' ').append(button2.el);});
button から出るEventStream を Bus として取得できるようにしておく
例: Cross Counting Button$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; }, }; var streams1 = mkButton.streams(); var streams2 = mkButton.streams(); var logic = function(s) { return s.clicked.map(constant(1)).scan(0, function(a, b) { return a + b; }); }; var button1 = mkButton.create({ properties: { text: logic(streams2) }, streams: streams1, }); var button2 = mkButton.create({ properties: { text: logic(streams1) }, streams: streams2, }); $('body').append(button1.el).append(' ').append(button2.el);});
先に EventStream だけ取得しておいて、
button から出るEventStream を Bus として取得できるようにしておく
例: Cross Counting Button$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; }, }; var streams1 = mkButton.streams(); var streams2 = mkButton.streams(); var logic = function(s) { return s.clicked.map(constant(1)).scan(0, function(a, b) { return a + b; }); }; var button1 = mkButton.create({ properties: { text: logic(streams2) }, streams: streams1, }); var button2 = mkButton.create({ properties: { text: logic(streams1) }, streams: streams2, }); $('body').append(button1.el).append(' ').append(button2.el);});
先に EventStream だけ取得しておいて、
EventStream から Property を作成
button から出るEventStream を Bus として取得できるようにしておく
例: Cross Counting Button$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; }, }; var streams1 = mkButton.streams(); var streams2 = mkButton.streams(); var logic = function(s) { return s.clicked.map(constant(1)).scan(0, function(a, b) { return a + b; }); }; var button1 = mkButton.create({ properties: { text: logic(streams2) }, streams: streams1, }); var button2 = mkButton.create({ properties: { text: logic(streams1) }, streams: streams2, }); $('body').append(button1.el).append(' ').append(button2.el);});
button から出るEventStream を Bus として取得できるようにしておく
先に EventStream だけ取得しておいて、
一緒に EventStream (Bus) を渡す
EventStream から Property を作成
例: Cross Counting Button$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; }, }; var streams1 = mkButton.streams(); var streams2 = mkButton.streams(); var logic = function(s) { return s.clicked.map(constant(1)).scan(0, function(a, b) { return a + b; }); }; var button1 = mkButton.create({ properties: { text: logic(streams2) }, streams: streams1, }); var button2 = mkButton.create({ properties: { text: logic(streams1) }, streams: streams2, }); $('body').append(button1.el).append(' ').append(button2.el);});
渡された Bus に、クリックの EventStream をつなげる
例: Cross Counting Button$(function() { var constant = function(x) { return function() { return x; }; }; var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; }, }; var streams1 = mkButton.streams(); var streams2 = mkButton.streams(); var logic = function(s) { return s.clicked.map(constant(1)).scan(0, function(a, b) { return a + b; }); }; var button1 = mkButton.create({ properties: { text: logic(streams2) }, streams: streams1, }); var button2 = mkButton.create({ properties: { text: logic(streams1) }, streams: streams2, }); $('body').append(button1.el).append(' ').append(button2.el);});
副作用(値変化時の処理登録、テキストの中身を変更)
副作用(DOM 要素登録)
副作用(Bus につなぐ)
リクエスト処理
Bacon.fromPromise
• jQuery の ajax 系メソッドが返す Promise オブジェクトから、EventStream を作る。
Bacon.fromPromise
• jQuery の ajax 系メソッドが返す Promise オブジェクトから、EventStream を作る。
assign は、イベント登録を解除する関数を返す
流れてきたレスポンス
• レスポンスがエラーの場合、そのままでは assign 等に流れていかない。
エラーレスポンスの処理
エラーレスポンスの処理
• レスポンスがエラーの場合、そのままでは assign 等に流れていかない。
• mapError メソッドを使って、エラー系の流れを変換関数を通して本流へ流す。
流れてきたエラーレスポンス
エラー系の流れをそのまま本流へ
EventStream#flatMap/flatMapLatest
• EventStream が流れてくる EventStream を、中の EventStream に流れる値が流れてくる EventStream にする
引用: https://github.com/raimohanska/bacon.js/wiki/Diagrams
引用: https://github.com/raimohanska/bacon.js/wiki/Diagrams
flatMap flatMapLatest
EventStream#flatMap/flatMapLatest
• EventStream が流れてくる EventStream を、中の EventStream に流れる値が流れてくる EventStream にする
引用: https://github.com/raimohanska/bacon.js/wiki/Diagrams
引用: https://github.com/raimohanska/bacon.js/wiki/Diagrams
flatMap flatMapLatest全部流す
後の結果の方が速ければ、前の結果は流れない
リクエスト処理の例右に入力したパスに
GET リクエストを飛ばす
返ってきたレスポンスを表示
ヘルパ
var constant = function(x) { return function() { return x; }; };var id = function(x) { return x; };var left = function(x) { return function(f, g) { return f(x); }; };var right = function(x) { return function(f, g) { return g(x); }; };
var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.properties.enable.assign(function(enable) { el.attr('disabled', !enable); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; },};
var mkTextarea = function(conf) { var el = $('<textarea />'); conf.val.assign(function(text) { el.val(text); }); return { el: el };};
ヘルパ
var constant = function(x) { return function() { return x; }; };var id = function(x) { return x; };var left = function(x) { return function(f, g) { return f(x); }; };var right = function(x) { return function(f, g) { return g(x); }; };
var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.properties.enable.assign(function(enable) { el.attr('disabled', !enable); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; },};
var mkTextarea = function(conf) { var el = $('<textarea />'); conf.val.assign(function(text) { el.val(text); }); return { el: el };};
Either
ヘルパ
var constant = function(x) { return function() { return x; }; };var id = function(x) { return x; };var left = function(x) { return function(f, g) { return f(x); }; };var right = function(x) { return function(f, g) { return g(x); }; };
var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.properties.enable.assign(function(enable) { el.attr('disabled', !enable); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; },};
var mkTextarea = function(conf) { var el = $('<textarea />'); conf.val.assign(function(text) { el.val(text); }); return { el: el };};
ボタンの 有効/無効 もProperty で受け取る
ヘルパ
var constant = function(x) { return function() { return x; }; };var id = function(x) { return x; };var left = function(x) { return function(f, g) { return f(x); }; };var right = function(x) { return function(f, g) { return g(x); }; };
var mkButton = { streams: function() { return { clicked: new Bacon.Bus() }; }, create: function(conf) { var el = $('<button />'); conf.properties.text.assign(function(text) { el.text(text); }); conf.properties.enable.assign(function(enable) { el.attr('disabled', !enable); }); conf.streams.clicked.plug(el.asEventStream('click')); return { el: el }; },};
var mkTextarea = function(conf) { var el = $('<textarea />'); conf.val.assign(function(text) { el.val(text); }); return { el: el };};
出力用テキストエリア設定は出力値 Property のみ。
実装var input = $('<input type="text" />').width(400);var bs = mkButton.streams();var request = bs.clicked.map(function() { return input.val(); });var response = request.flatMapLatest(function(url) { return Bacon.fromPromise($.get(url)).map(right) .mapError(function(e) { return left(e.responseText); });});var button = mkButton.create({ properties: { text: Bacon.constant('request'), enable: Bacon.once(true) .merge(request.map(constant(false))) .merge(response.map(constant(true))) .toProperty(), }, streams: bs,});var output = mkTextarea({ val: response.map(function(r) { return r(function(msg) { return 'error - ' + msg; }, id); }).toProperty(),});$('body') .append($('<p />').append(input).append(' ').append(button.el)) .append($('<p />').append(output.el.width(500).height(200)));
実装var input = $('<input type="text" />').width(400);var bs = mkButton.streams();var request = bs.clicked.map(function() { return input.val(); });var response = request.flatMapLatest(function(url) { return Bacon.fromPromise($.get(url)).map(right) .mapError(function(e) { return left(e.responseText); });});var button = mkButton.create({ properties: { text: Bacon.constant('request'), enable: Bacon.once(true) .merge(request.map(constant(false))) .merge(response.map(constant(true))) .toProperty(), }, streams: bs,});var output = mkTextarea({ val: response.map(function(r) { return r(function(msg) { return 'error - ' + msg; }, id); }).toProperty(),});$('body') .append($('<p />').append(input).append(' ').append(button.el)) .append($('<p />').append(output.el.width(500).height(200)));
ボタンがクリックされると入力値が流れる EventStream
実装var input = $('<input type="text" />').width(400);var bs = mkButton.streams();var request = bs.clicked.map(function() { return input.val(); });var response = request.flatMapLatest(function(url) { return Bacon.fromPromise($.get(url)).map(right) .mapError(function(e) { return left(e.responseText); });});var button = mkButton.create({ properties: { text: Bacon.constant('request'), enable: Bacon.once(true) .merge(request.map(constant(false))) .merge(response.map(constant(true))) .toProperty(), }, streams: bs,});var output = mkTextarea({ val: response.map(function(r) { return r(function(msg) { return 'error - ' + msg; }, id); }).toProperty(),});$('body') .append($('<p />').append(input).append(' ').append(button.el)) .append($('<p />').append(output.el.width(500).height(200)));
入力が流れてきたら、リクエストを飛ばす
実装var input = $('<input type="text" />').width(400);var bs = mkButton.streams();var request = bs.clicked.map(function() { return input.val(); });var response = request.flatMapLatest(function(url) { return Bacon.fromPromise($.get(url)).map(right) .mapError(function(e) { return left(e.responseText); });});var button = mkButton.create({ properties: { text: Bacon.constant('request'), enable: Bacon.once(true) .merge(request.map(constant(false))) .merge(response.map(constant(true))) .toProperty(), }, streams: bs,});var output = mkTextarea({ val: response.map(function(r) { return r(function(msg) { return 'error - ' + msg; }, id); }).toProperty(),});$('body') .append($('<p />').append(input).append(' ').append(button.el)) .append($('<p />').append(output.el.width(500).height(200)));
正常レスポンスを Right で包んでおいて、
実装var input = $('<input type="text" />').width(400);var bs = mkButton.streams();var request = bs.clicked.map(function() { return input.val(); });var response = request.flatMapLatest(function(url) { return Bacon.fromPromise($.get(url)).map(right) .mapError(function(e) { return left(e.responseText); });});var button = mkButton.create({ properties: { text: Bacon.constant('request'), enable: Bacon.once(true) .merge(request.map(constant(false))) .merge(response.map(constant(true))) .toProperty(), }, streams: bs,});var output = mkTextarea({ val: response.map(function(r) { return r(function(msg) { return 'error - ' + msg; }, id); }).toProperty(),});$('body') .append($('<p />').append(input).append(' ').append(button.el)) .append($('<p />').append(output.el.width(500).height(200)));
正常レスポンスを Right で包んでおいて、
エラーレスポンスは Left で包んで本流へ
実装var input = $('<input type="text" />').width(400);var bs = mkButton.streams();var request = bs.clicked.map(function() { return input.val(); });var response = request.flatMapLatest(function(url) { return Bacon.fromPromise($.get(url)).map(right) .mapError(function(e) { return left(e.responseText); });});var button = mkButton.create({ properties: { text: Bacon.constant('request'), enable: Bacon.once(true) .merge(request.map(constant(false))) .merge(response.map(constant(true))) .toProperty(), }, streams: bs,});var output = mkTextarea({ val: response.map(function(r) { return r(function(msg) { return 'error - ' + msg; }, id); }).toProperty(),});$('body') .append($('<p />').append(input).append(' ').append(button.el)) .append($('<p />').append(output.el.width(500).height(200)));
レスポンスが返ってくるまではボタンを無効にする
実装var input = $('<input type="text" />').width(400);var bs = mkButton.streams();var request = bs.clicked.map(function() { return input.val(); });var response = request.flatMapLatest(function(url) { return Bacon.fromPromise($.get(url)).map(right) .mapError(function(e) { return left(e.responseText); });});var button = mkButton.create({ properties: { text: Bacon.constant('request'), enable: Bacon.once(true) .merge(request.map(constant(false))) .merge(response.map(constant(true))) .toProperty(), }, streams: bs,});var output = mkTextarea({ val: response.map(function(r) { return r(function(msg) { return 'error - ' + msg; }, id); }).toProperty(),});$('body') .append($('<p />').append(input).append(' ').append(button.el)) .append($('<p />').append(output.el.width(500).height(200)));
正常レスポンスはそのまま出力エラーレスポンスは ‘error - ’ に続けて出力
設計
• 実際には、streams から properties を作る部分や、リクエストを飛ばす部分は、別モジュールにしておくといい。
ViewLogic
StorageAjax
App
get streams,create from properties
create propertiesfrom streams and storages
create response-streams