SlideShare ist ein Scribd-Unternehmen logo
1 von 57
Downloaden Sie, um offline zu lesen
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

Weitere ähnliche Inhalte

Mehr von rf0444

Start FRP
Start FRPStart FRP
Start FRPrf0444
 
PFDS 11.2.2
PFDS 11.2.2PFDS 11.2.2
PFDS 11.2.2rf0444
 
PFDS 10.1.2
PFDS 10.1.2PFDS 10.1.2
PFDS 10.1.2rf0444
 
PFDS 9.3.2
PFDS 9.3.2PFDS 9.3.2
PFDS 9.3.2rf0444
 
PFDS 9.3.1
PFDS 9.3.1PFDS 9.3.1
PFDS 9.3.1rf0444
 
PFDS 8.4.1
PFDS 8.4.1PFDS 8.4.1
PFDS 8.4.1rf0444
 
PFDS 7.4
PFDS 7.4PFDS 7.4
PFDS 7.4rf0444
 
Tapl 5
Tapl 5Tapl 5
Tapl 5rf0444
 
Haskellday rf
Haskellday rfHaskellday rf
Haskellday rfrf0444
 
PFDS 6.4.3
PFDS 6.4.3PFDS 6.4.3
PFDS 6.4.3rf0444
 

Mehr von rf0444 (10)

Start FRP
Start FRPStart FRP
Start FRP
 
PFDS 11.2.2
PFDS 11.2.2PFDS 11.2.2
PFDS 11.2.2
 
PFDS 10.1.2
PFDS 10.1.2PFDS 10.1.2
PFDS 10.1.2
 
PFDS 9.3.2
PFDS 9.3.2PFDS 9.3.2
PFDS 9.3.2
 
PFDS 9.3.1
PFDS 9.3.1PFDS 9.3.1
PFDS 9.3.1
 
PFDS 8.4.1
PFDS 8.4.1PFDS 8.4.1
PFDS 8.4.1
 
PFDS 7.4
PFDS 7.4PFDS 7.4
PFDS 7.4
 
Tapl 5
Tapl 5Tapl 5
Tapl 5
 
Haskellday rf
Haskellday rfHaskellday rf
Haskellday rf
 
PFDS 6.4.3
PFDS 6.4.3PFDS 6.4.3
PFDS 6.4.3
 

FRP in Practice

  • 1. Functional Reactive Programming 実践編 ∼ 画面作成、リクエスト処理 ∼ @rf0444
  • 3. おしながき • EventStream と Property • 画面を作る • リクエスト処理
  • 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)
  • 12. 設計方針 • 出来るだけ副作用を排除したい。 • Callback 内処理を、単純な副作用だけにしたい。
  • 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 要素登録)
  • 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() { 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 要素登録)
  • 28. 例: Cross Counting Button クリックすると 反対側が増える
  • 29. 例: Cross Counting Button クリックすると 反対側が増える Property を作るために、 別の button の EventStream が必要 (相互に要求)
  • 30. 例: Cross Counting Button クリックすると 反対側が増える Property を作るために、 別の button の EventStream が必要 (相互に要求) Bus を使い、 一旦 EventStream として渡しておき、 button を作るときに Bus につなぐ
  • 31. 例: 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); });
  • 32. 例: 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 として 取得できるようにしておく
  • 33. 例: 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 として 取得できるようにしておく
  • 34. 例: 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 として 取得できるようにしておく
  • 35. 例: 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 を作成
  • 36. 例: 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 をつなげる
  • 37. 例: 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 につなぐ)
  • 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 全部流す 後の結果の方が速ければ、前の結果は流れない
  • 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