Created
July 23, 2015 13:52
-
-
Save anonymous/2047c5493346b4d6546d to your computer and use it in GitHub Desktop.
FRP? くりっく かうんたー べーこん w
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>FRP? くりっく かうんたー べーこん w</title> | |
<link rel="stylesheet" | |
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css" /> | |
</head> | |
<body> | |
<div class="container"> | |
<h2>FRP? くりっく かうんたー べーこん w</h2> | |
<div id="content"></div> | |
<div id="save-load-content"></div> | |
<p> | |
べーこんでつつんでやくと、えふあーるぴーっていうやつになるらしいぞ<br /> | |
なにそれ、おいしいの?<br /> | |
たぶん、うまい<br /> | |
</p> | |
<p></p> | |
<p> | |
時を巻き戻して、そして進める能力を手に入れた! | |
なお、記録される履歴はメモリが許す限り無限なので、メモリ不足になる前に適当に忘却してください。 | |
</p> | |
<p> | |
時を記録する能力を手に入れた! | |
履歴もあわせて保存されます。 | |
ClickCounter本体と親子関係にないため、ストリームを通じてアクションや状態をやりとりしています。 | |
</p> | |
<p> | |
作成には以下の資料を参考にしています。 | |
ただし、実験的にECMAPScript2015のclassを使っていたりしているので、作りが全く同じではありません。 | |
</p> | |
<ul> | |
<li><a href="https://medium.com/@milankinen/good-bye-flux-welcome-bacon-rx-23c71abfb1a7"> | |
Good bye Flux, welcome Bacon/Rx? | |
</a></li> | |
<li><a href="https://github.com/milankinen/react-bacon-todomvc"> | |
Classical TodoMVC with React+Bacon.js | |
</a></li> | |
</ul> | |
<p> | |
これが真のFRPなのか?一番最後のコード | |
</p> | |
<pre><code>appState.onValue((state) => { | |
React.render(<ClickCounter {...state.props} dispatcher={dispatcher}>, | |
document.getElementById('content')); | |
React.render(<SaveLoad enableLoad={state.props.enableLoad} dispatcher={dispatcher}/>, | |
document.getElementById('save-load-content')); | |
});</code></pre> | |
</p> | |
この部分はFRPのように見えます。まさしく、FRPの考え方に基づくコードでしょう。ですが、 | |
問題は<code>Dispatcher</code>です。 | |
その中身はきわめて命令型的です。 | |
もっと関数型風に作ることもできますが、<code>Bacon.bus</code>に<code>push</code>している時点で、 | |
真の関数型と言っていいのか私にはわかりません。 | |
モナドを使えればできるかも知れませんが、JavaScriptはモナドを使うには適さない言語です。 | |
それにやり方はよくわかりません。私の今のレベルではこれが限界なのかも知れません。 | |
</p> | |
<p> | |
FRPとしてはあやしいといってもFRP的なところで恩恵を受けているところがあります。 | |
それは、状態がimmutableなことです。 | |
ソースを見てもらえれば分かりますが、undo/redo/forget/save/loadのコードは実質1行です。 | |
たった1行で履歴関係は簡単に実装できるのがFRPの醍醐味ではないでしょうか? | |
</p> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.15/browser.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.15/browser-polyfill.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bacon.js/0.7.70/Bacon.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.17.0/ramda.min.js"></script> | |
<script type="text/babel"> | |
"use strict"; | |
class Dispatcher { | |
constructor() { | |
this.busCache = {}; | |
} | |
stream(name) { | |
return this._bus(name); | |
} | |
push(name, value) { | |
this._bus(name).push(value); | |
} | |
plug(name, value) { | |
this._bus(name).plug(value); | |
} | |
_bus(name) { | |
return this.busCache[name] = this.busCache[name] || new Bacon.Bus() | |
} | |
} | |
class Counts { | |
constructor(dispatcher) { | |
this.dispatcher = dispatcher; | |
} | |
toProperty(number) { | |
const initialData = { | |
counts: R.repeat(0, number), | |
prev: null, | |
next: null, | |
save: null | |
}; | |
const dataState = Bacon.update(initialData, | |
[this.dispatcher.stream('count')], this._count, | |
[this.dispatcher.stream('undo')], this._undo, | |
[this.dispatcher.stream('redo')], this._redo, | |
[this.dispatcher.stream('forget')], this._forget, | |
[this.dispatcher.stream('save')], this._save, | |
[this.dispatcher.stream('load')], this._load | |
); | |
return dataState.map(this._withDisplayStatus); | |
} | |
_count(data, number) { | |
return R.merge(data, { | |
counts: R.set(R.lensIndex(number), R.nth(number, data.counts) + 1, data.counts), | |
prev: data, | |
next: null | |
}); | |
} | |
_undo(data) { | |
return R.merge(data.prev, {next: data}); | |
} | |
_redo(data) { | |
return R.merge(data.next, {prev: data}); | |
} | |
_forget(data) { | |
return R.merge(data, {prev: null, next: null}); | |
} | |
_save(data) { | |
return R.merge(data, {save: data}); | |
} | |
_load(data) { | |
return R.merge(data.save, {save: data.save}); | |
} | |
_withDisplayStatus(data) { | |
return { | |
counts: data.counts, | |
total: R.sum(data.counts), | |
enableUndo: data.prev != null, | |
enableRedo: data.next != null, | |
enableLoad: data.save != null | |
}; | |
} | |
} | |
class ClickCounter extends React.Component { | |
_clickCounter(number) { | |
this.props.dispatcher.push("count", number); | |
} | |
_clickUndo() { | |
this.props.dispatcher.push("undo"); | |
} | |
_clickRedo() { | |
this.props.dispatcher.push("redo"); | |
} | |
_clickForget() { | |
this.props.dispatcher.push("forget"); | |
} | |
render() { | |
return ( | |
<div className="clickCounter"> | |
<ChildrenCounter counts={this.props.counts} total={this.props.total} | |
onClick={this._clickCounter.bind(this)}/> | |
<UndoRedoButton enableUndo={this.props.enableUndo} enableRedo={this.props.enableRedo} | |
clickUndo={this._clickUndo.bind(this)} clickRedo={this._clickRedo.bind(this)} | |
clickForget={this._clickForget.bind(this)}/> | |
</div> | |
); | |
} | |
} | |
class ChildrenCounter extends React.Component { | |
render() { | |
const children = this.props.counts.map((v, i) => { | |
return <ChildCounter number={i} count={v} total={this.props.total} onClick={this.props.onClick} key={i}/>; | |
}); | |
return ( | |
<div className="childrenCounter row"> | |
{children} | |
</div> | |
); | |
} | |
} | |
class ChildCounter extends React.Component { | |
_onClick() { | |
return this.props.onClick(this.props.number); | |
} | |
render() { | |
return ( | |
<div className="childCounter col-sm-3 col-md-2 panel panel-default text-center btn btn-default" | |
onClick={this._onClick.bind(this)}> | |
<h3>ばんごう {this.props.number}</h3> | |
<p>このわく <span className="badge">{this.props.count}</span></p> | |
<p>とーたる <span className="badge">{this.props.total}</span></p> | |
</div> | |
); | |
} | |
} | |
class UndoRedoButton extends React.Component { | |
createButton(name, text, event, style) { | |
const classes = name + "-button btn btn-" + style; | |
if (event) { | |
return <button name={name} className={classes} onClick={event}>{text}</button> | |
} else { | |
return <button name={name} className={classes} disabled="disabled">{text}</button> | |
} | |
} | |
render() { | |
const undoButton = this.createButton("undo", "時よ戻れ!", | |
this.props.enableUndo && this.props.clickUndo, "primary"); | |
const redoButton = this.createButton("redo", "時よ進め!", | |
this.props.enableRedo && this.props.clickRedo, "primary"); | |
const forgetButton = this.createButton("forget", "記憶を忘却", | |
(this.props.enableUndo || this.props.enableRedo) && this.props.clickForget, "default"); | |
return ( | |
<p className="undo-redo-button"> | |
{undoButton} | |
{redoButton} | |
{forgetButton} | |
</p> | |
); | |
} | |
} | |
class SaveLoad extends React.Component { | |
_clickSave() { | |
this.props.dispatcher.push("save"); | |
} | |
_clickLoad() { | |
this.props.dispatcher.push("load"); | |
} | |
createButton(name, text, event, style) { | |
const classes = name + "-button btn btn-" + style; | |
if (event) { | |
return <button name={name} className={classes} onClick={event}>{text}</button> | |
} else { | |
return <button name={name} className={classes} disabled="disabled">{text}</button> | |
} | |
} | |
render() { | |
const saveButton = this.createButton("save", "保存", this._clickSave.bind(this), "default"); | |
const loadButton = this.createButton("load", "読込", this.props.enableLoad && this._clickLoad.bind(this), | |
"default"); | |
return ( | |
<p className="save-load"> | |
{saveButton} | |
{loadButton} | |
</p> | |
); | |
} | |
} | |
const dispatcher = new Dispatcher(); | |
const counts = new Counts(dispatcher); | |
const appState = Bacon.combineTemplate({props: counts.toProperty(4)}); | |
appState.onValue((state) => { | |
React.render(<ClickCounter {...state.props} dispatcher={dispatcher}/>, | |
document.getElementById('content')); | |
React.render(<SaveLoad enableLoad={state.props.enableLoad} dispatcher={dispatcher}/>, | |
document.getElementById('save-load-content')); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment