Skip to content

Instantly share code, notes, and snippets.

Created July 23, 2015 13:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/2047c5493346b4d6546d to your computer and use it in GitHub Desktop.
Save anonymous/2047c5493346b4d6546d to your computer and use it in GitHub Desktop.
FRP? くりっく かうんたー べーこん w
<!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) =&gt; {
React.render(&lt;ClickCounter {...state.props} dispatcher={dispatcher}&gt;,
document.getElementById('content'));
React.render(&lt;SaveLoad enableLoad={state.props.enableLoad} dispatcher={dispatcher}/&gt;,
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}&nbsp;
{redoButton}&nbsp;
{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}&nbsp;
{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