関数型ReactでビジュアルFizzBuzz ref: http://qiita.com/ababup1192/items/bf6d25845a4556a9e0ca
// 複数のイベントに対応するためにBusを増やす。
const addItemBus: Bacon.Bus<any, any> = new Bacon.Bus();
const removeItemBus: Bacon.Bus<any, any> = new Bacon.Bus();
// 2つのイベント
const addItem = (list: List<string>, message: string): List<string> =>
list.push(message);
const removeItem = (list: List<string>, message: string): List<string> =>
list.remove(list.indexOf(message));
// イベントとBusの紐付け。型パラメータが少し複雑・・・。
// update<addItem, removeItem, 畳み込んだ後の型, init>
// => update<string, string, List<string>, List<string>>
const property = Bacon.update<string, string, List<string>, List<string>>(List<string>(),
[addItemBus], addItem,
[removeItemBus], removeItem
);
// 変わらず、畳み込み演算(イベントの監視)開始。
property.onValue((list) => console.log(list));
console.log("----");
addItemBus.push("hello");
addItemBus.push("baconjs");
addItemBus.push("world");
console.log("~~~~");
removeItemBus.push("baconjs");
// List []
// -------
// List [ "hello" ]
// List [ "hello", "baconjs" ]
// List [ "hello", "baconjs", "world" ]
// ~~~~
// List [ "hello", "world" ]
// カスタムイベントBusの生成。
const bus: Bacon.Bus<any, any> = new Bacon.Bus();
// 畳み込み演算に利用するための関数。
// 現在の文字列の長さを得るのに前の値は要らない。
const func = (_, message: string): number => message.length;
// 生成したBusと関数funcの紐付け。長さの初期値を-1としておく。
// 型パラメータは、update<func, init> => <message, length, init>
const property = Bacon.update<string, number, number>(-1,
[bus], func
);
// 畳み込み演算の開始
property.onValue((length) => console.log(length));
// Busにpushでイベントを発火
console.log("----");
bus.push("hello");
console.log("^^^^");
bus.push("FRP");
console.log("~~~~");
bus.push("world!");
// -1
// ----
// 5
// ^^^^
// 3
// ~~~~
// 6
};
["hello", "FRP", "world!"].reduce((_, message: string) =>
console.log(message.length)
)
[event, event, event, event].reduce((currentState, e) =>
// currentStateとeを使用した新しいstate
);
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((sum, x) => sum + x);
// => 55
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((sum, x) => sum + x);
// => 55
interface IMaxInputProps {
max: number;
handleMaxChange: (max: number) => void;
}
interface IMaxInputState {
max: number;
}
/**** MaxInput.tsx *****/
<input
type="number"
value={this.state.max}
onChange={(e) => {
const max = Number(e.target.value);
const validatedMax = Math.abs(max) > 1000 ? 1000 : Math.abs(max);
handleMaxChange(validatedMax);
this.setState({ max: validatedMax });
} } />
/**** App.tsx *****/
class App extends React.Component {
// 中略
private handleMaxChange(max: number) {
// Object.assign(this.state, {list: Range(...)}); と同じ。
this.setState({ ...this.state, ...{ list: Range(1, max + 1).toList() } });
}
render{
// 中略
return ...
<MaxInput
max={max}
handleMaxChange={(max) => this.handleMaxChange(max)} />
}
}
/**** index.tsx *****/
ReactDOM.render(<App max={100} fizz={3} buzz={5} />, document.getElementById("content"));
/**** App.tsx *****/
interface IAppProps {
max: number;
fizz: number;
buzz: number;
}
interface IAppState {
list: List<number>;
fizz: number;
buzz: number;
}
export default class App extends React.Component<IAppProps, IAppState> {
constructor(props) {
super(props);
const list = Range(1, this.props.max + 1).toList();
const {fizz, buzz, max} = this.props;
this.state = { list, fizz, buzz };
}
// 中略
render() {
const {max, fizz, buzz} = this.props;
return <div>
<div className="fizzbuzzInputs">
<MaxInput
max={max}
handleMaxChange={(max) => this.handleMaxChange(max)} />
<FizzBuzzInput
fizz={fizz}
buzz={buzz}
handleFizzChange={(fizz) => this.handlFizzChange(fizz)}
handleBuzzChange={(buzz) => this.handlBuzzChange(buzz)}
/>
</div>
<FizzBuzzContainer {...this.state} />
</div>;
}
}
const initialMax = 100;
const initialFizz = 3;
const initialBuzz = 5;
const dispatcher = new Dispatcher();
const maxAction = new MaxAction(dispatcher, initialMax);
const maxProperty = maxAction.createProperty();
const fizzbuzzAction = new FizzbuzzAction(dispatcher, initialFizz, initialBuzz);
const fizzbuzzProperty = fizzbuzzAction.createProperty();
Bacon.onValues(maxProperty, fizzbuzzProperty, (max: number, fizzBuzz: IFizzbuzz) => {
const { fizz, buzz } = fizzBuzz;
const fizzbuzzList = FizzBuzz.createFizzbuzzList(max, fizz, buzz);
const props = { fizz, buzz, max, fizzbuzzList, maxAction, fizzbuzzAction };
ReactDOM.render(<App {...props} />, document.getElementById("content"));
});
import { List, is } from "immutable";
import { FizzBuzz } from "../fizzbuzz";
describe("fizzbuzz", () => {
it("should return fizzbuzz list", () => {
const expected = List.of("1", "2", "fizz", "4", "buzz",
"fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", "14", "fizzbuzz");
const actual = FizzBuzz.createFizzbuzzList(15, 3, 5);
expect(is(expected, actual)).toBeTruthy();
});
it("should return normal number", () => {
const expected = 50;
const actual = FizzBuzz.validateMax(50);
expect(expected).toBe(actual);
});
it("should return abs of negative number", () => {
const expected = 50;
const actual = FizzBuzz.validateMax(-50);
expect(expected).toBe(actual);
});
it("should return limited number", () => {
const expected = 1000;
const actual = FizzBuzz.validateMax(99999);
expect(expected).toBe(actual);
});
});
/**** maxAction.ts ****/
import * as Bacon from "baconjs";
import Dispatcher from "./dispatcher";
import { FizzBuzz } from "../models/fizzbuzz";
const CHANGE_MAX = "CHANGE_MAX";
export default class MaxAction {
private d: Dispatcher;
private initialValue: number;
constructor(dispatcher: Dispatcher, initialValue: number) {
this.d = dispatcher;
this.initialValue = initialValue;
}
public changeMax(max: number) {
this.d.push(CHANGE_MAX, max);
}
public createProperty(): Bacon.Property<number, number> {
return Bacon.update<number, number, number>(this.initialValue,
[this.d.stream(CHANGE_MAX)], this._changeMax.bind(this)
);
}
private _changeMax(_, newMax: number): number {
return FizzBuzz.validateMax(newMax);
}
}
/**** fizzbuzzAction.ts ****/
const CHANGE_FIZZ = "CHANGE_FIZZ";
const CHANGE_BUZZ = "CHANGE_BUZZ";
export interface IFizzbuzz {
fizz: number;
buzz: number;
}
export class FizzbuzzAction {
private d: Dispatcher;
private initialFizz: number;
private initialBuzz: number;
constructor(dispatcher: Dispatcher, initialFizz: number, initialBuzz: number) {
this.d = dispatcher;
this.initialFizz = initialFizz;
this.initialBuzz = initialBuzz;
}
public changeFizz(fizz: number) {
this.d.push(CHANGE_FIZZ, fizz);
}
public changeBuzz(buzz: number) {
this.d.push(CHANGE_BUZZ, buzz);
}
public createProperty(): Bacon.Property<IFizzbuzz, IFizzbuzz> {
const initialValue = { fizz: this.initialFizz, buzz: this.initialBuzz };
return Bacon.update<IFizzbuzz, number, number, IFizzbuzz>(initialValue,
[this.d.stream(CHANGE_FIZZ)], this._changeFizz.bind(this),
[this.d.stream(CHANGE_BUZZ)], this._changeBuzz.bind(this));
}
private _changeFizz(oldFizzbuzz: IFizzbuzz, newFizz: number): IFizzbuzz {
return { ...oldFizzbuzz, ...{ fizz: newFizz } };
}
private _changeBuzz(oldFizzbuzz: IFizzbuzz, newBuzz: number): IFizzbuzz {
return { ...oldFizzbuzz, ...{ buzz: newBuzz } };
}
}
/**** fizzbuzz.ts ****/
import { Range, List } from "immutable";
export namespace FizzBuzz {
export function createFizzbuzzList(max: number, fizz: number, buzz: number): List<string> {
const fizzbuzz = fizz * buzz;
return Range(1, max + 1).map((n) =>
n % fizzbuzz === 0 ? "fizzbuzz" :
n % fizz === 0 ? "fizz" :
n % buzz === 0 ? "buzz" :
n.toString()).toList();
}
export function validateMax(max: number): number {
return Math.abs(max) > 1000 ? 1000 : Math.abs(max);
}
}
export default class Dispatcher {
private handlers: Map<string, Bacon.Bus<any, any>>;
constructor() {
this.handlers = Map<string, Bacon.Bus<any, any>>();
}
// busを文字列で指定する
public stream(name: string): Bacon.Bus<any, any> {
return this.bus(name);
}
// 文字列で指定したbusにデータを流す
public push(name: string, value: any): void {
this.bus(name).push(value);
}
// busをplugする
public plug(name: string, value: any): void {
this.bus(name).plug(value);
}
// busを生成する
private bus(name: string): Bacon.Bus<any, any> {
if (this.handlers.has(name)) {
return this.handlers.get(name);
} else {
const newBus = new Bacon.Bus();
this.handlers = this.handlers.set(name, newBus);
return newBus;
}
}
}
class MessageAction {
private bus: Bacon.Bus<any, any>;
constructor() {
this.bus = new Bacon.Bus();
}
// イベントを発火するメソッドを用意
pushMessage(message: string) {
this.bus.push(message);
}
createProperty(): Bacon.Property<string, number> {
return Bacon.update<string, number, number>(-1,
[this.bus], this._pushMessage.bind(this)
);
}
// 畳み込みに使われる関数内容は、private
private _pushMessage(_, message: string): number {
return message.length;
}
}
class ListAction {
private addItemBus: Bacon.Bus<any, any>;
private removeItemBus: Bacon.Bus<any, any>;
constructor() {
this.addItemBus = new Bacon.Bus();
this.removeItemBus = new Bacon.Bus();
}
addItem(message: string) {
this.addItemBus.push(message);
}
removeItem(message: string) {
this.removeItemBus.push(message);
}
createProperty(): Bacon.Property<string, List<string>> {
return Bacon.update<string, string, List<string>, List<string>>(List<string>(),
[this.addItemBus], this._addItem.bind(this),
[this.removeItemBus], this._removeItem.bind(this)
);
}
private _addItem(list: List<string>, message: string): List<string> {
return list.push(message);
};
private _removeItem(list: List<string>, message: string): List<string> {
return list.remove(list.indexOf(message));
};
}
// Busの生成は、コンストラクタに任せる。
const messageAction = new MessageAction();
const listAction = new ListAction();
const messageProperty = messageAction.createProperty();
const listProperty = listAction.createProperty();
// Bacon.onValuesメソッドで、複数の畳込み演算をマージできる。
// ユーザは、マージされた結果のみに集中して処理を書ける。
Bacon.onValues(messageProperty, listProperty,
(length: number, list: List<string>) => {
console.log("-------");
console.log(`length = ${length}`);
console.log(`list = ${list}`);
console.log("-------");
});
// イベントの発火は、Action経由で行う。
messageAction.pushMessage("hello");
listAction.addItem("hello");
messageAction.pushMessage("baconjs");
listAction.addItem("baconjs");
messageAction.pushMessage("world");
listAction.addItem("world");
listAction.removeItem("baconjs");
// -------
// length = -1
// list = List []
// -------
// -------
// length = 5
// list = List []
// -------
// -------
// length = 5
// list = List [ "hello" ]
// -------
// -------
// length = 7
// list = List [ "hello" ]
// -------
// -------
// length = 7
// list = List [ "hello", "baconjs" ]
// -------
// -------
// length = 5
// list = List [ "hello", "baconjs" ]
// -------
// -------
// length = 5
// list = List [ "hello", "baconjs", "world" ]
// -------
// -------
// length = 5
// list = List [ "hello", "world" ]
// -------
<div id="messages">
<p>Hello.</p>
<p>How are you?</p>
<p>I'm FIne!</p>
</div>
<div id="messages">
<p>Hello.</p>
<p>How are you?</p>
<p>I'm FIne!</p>
</div>
function createParagraph(text) {
const paragraph = document.createElement("p");
const textNode = document.createTextNode(text);
paragraph.appendChild(textNode);
return paragraph;
}
const messageTexts = ["Hello.", "How are you?", "I'm Fine!"];
const messages = document.getElementById("messages");
messageTexts.map(createParagraph).forEach(function (message) {
messages.appendChild(message);
});
function createParagraph(text) {
const paragraph = document.createElement("p");
const textNode = document.createTextNode(text);
paragraph.appendChild(textNode);
return paragraph;
}
const messageTexts = ["Hello.", "How are you?", "I'm Fine!"];
const messages = document.getElementById("messages");
messageTexts.map(createParagraph).forEach(function (message) {
messages.appendChild(message);
});