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
Storage
Ajax
App
get streams,
create from properties
create properties
from streams and storages
create response-streams

FRP in Practice

  • 1.
    Functional Reactive Programming 実践編 ∼画面作成、リクエスト処理 ∼ @rf0444
  • 2.
  • 3.
    おしながき • EventStream とProperty • 画面を作る • リクエスト処理
  • 4.
  • 5.
    EventStream • 発生するイベントの列 を表す •クリックされた、など 時間 値
  • 6.
    Property • 時間によって変化する値 を表す •View に表示する値、最後に返ってきたレスポンス など 時間 値
  • 7.
    EventStream / Property •EventStream#merge(EventStream) • 2つの EventStream をくっつけた EventStream を作る。 時間 値 時間 値 時間 値 e1 e2 e1.merge(e2)
  • 8.
    EventStream / Property •EventStream#toProperty([initVal]) • EventStream に 値が流れてくる タイミングで、値が変化する Property を作る。 • 引数に初期値を指定できる。 (なしも可) 時間 値 時間 値 v0 es es.toProperty(v0)
  • 9.
    EventStream / Property •Property#changes() • Property の値が変化した タイミングで、変化後の 値が流れる EventStream を作る。 • 初期値は流れない 時間 値 p.changes() 時間 値 p
  • 10.
    EventStream / Property •Property#sampledBy(EventStream) • EventStream に値が流れた時点 の Property の値が流れる EventStream を作る。 時間 値 時間 値 時間 値 p es p.sampledBy(es)
  • 11.
  • 12.
    設計方針 • 出来るだけ副作用を排除したい。 • Callback内処理を、単純な副作用だけにしたい。
  • 13.
  • 14.
    例: 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); });
  • 15.
    例: 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); }); ボタン右のテキストの値 (時間によって変化する)
  • 16.
    例: 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
  • 17.
    例: 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 ができる)
  • 18.
    例: 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 要素登録)
  • 19.
  • 20.
    例: Counting Button クリックすると増える Propertyを作るために、 作成後の button の EventStream が必要
  • 21.
    例: Counting Button クリックすると増える Propertyを作るために、 作成後の button の EventStream が必要 EventStream -> Property な 関数を渡すようにしてみる
  • 22.
    例: 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); });
  • 23.
    例: 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 な関数
  • 24.
    例: 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を作る
  • 25.
    例: 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 を 作っておいて、
  • 26.
    例: 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 を 作っておいて、
  • 27.
    $(function() { varconstant = 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 要素登録)
  • 28.
    例: Cross CountingButton クリックすると 反対側が増える
  • 29.
    例: Cross CountingButton クリックすると 反対側が増える Property を作るために、 別の button の EventStream が必要 (相互に要求)
  • 30.
    例: Cross CountingButton クリックすると 反対側が増える Property を作るために、 別の button の EventStream が必要 (相互に要求) Bus を使い、 一旦 EventStream として渡しておき、 button を作るときに Bus につなぐ
  • 31.
    例: Cross CountingButton $(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); });
  • 32.
    例: Cross CountingButton $(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 として 取得できるようにしておく
  • 33.
    例: Cross CountingButton $(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 として 取得できるようにしておく
  • 34.
    例: Cross CountingButton $(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 として 取得できるようにしておく
  • 35.
    例: Cross CountingButton $(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 を作成
  • 36.
    例: Cross CountingButton $(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 をつなげる
  • 37.
    例: Cross CountingButton $(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 につなぐ)
  • 38.
  • 39.
    Bacon.fromPromise • jQuery のajax 系メソッドが返 す Promise オブジェクトから、 EventStream を作る。
  • 40.
    Bacon.fromPromise • jQuery のajax 系メソッドが返 す Promise オブジェクトから、 EventStream を作る。 assign は、イベント登録 を解除する関数を返す 流れてきたレスポンス
  • 41.
    • レスポンスがエラーの場合、そのままでは assign等に流れていかない。 エラーレスポンスの処理
  • 42.
    エラーレスポンスの処理 • レスポンスがエラーの場合、そのままでは assign等に流れていかない。 • mapError メソッドを使って、エラー系の流れを変換関数を通して本流へ流 す。 流れてきたエラーレスポンス エラー系の流れをそのまま本流へ
  • 43.
    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
  • 44.
    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 全部流す 後の結果の方が速ければ、前の結果は流れない
  • 45.
  • 46.
    ヘルパ 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 }; };
  • 47.
    ヘルパ 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
  • 48.
    ヘルパ 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 で受け取る
  • 49.
    ヘルパ 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 のみ。
  • 50.
    実装 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)));
  • 51.
    実装 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
  • 52.
    実装 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))); 入力が流れてきたら、 リクエストを飛ばす
  • 53.
    実装 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 で包んでおいて、
  • 54.
    実装 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 で包んで本流へ
  • 55.
    実装 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))); レスポンスが返ってくるまでは ボタンを無効にする
  • 56.
    実装 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 - ’ に続けて出力
  • 57.
    設計 • 実際には、streams からproperties を作る部分や、リクエストを飛ばす部分 は、別モジュールにしておくといい。 ViewLogic Storage Ajax App get streams, create from properties create properties from streams and storages create response-streams