Effect{ive,ful} Cycle.js

# おことわり

- この発表はかなり<b>お気持ち</b>です
- 他の発表とは異なり(？)なんら学術的裏付けがあるものではありません
- なんと(？)まったく数式が出てきません

--- 

# 自己紹介

- 亀岡 亮太
- [株式会社HERP](https://herp.co.jp/) リードエンジニア
- Twitter: [@ryotakameoka](https://twitter.com/ryotakameoka)
- GitHub: [@ryota-ka](https://github.com/ryota-ka)

---

# Web フロントエンドアプリケーションの話をします

---

# はじめに

---

# はじめに

## この発表の内容

- Web フロントエンドフレームワークはどのような性質を満たすべきか？
  - 異なる副作用を分離したい
  - 非同期がほしい
- Cycle.js というフレームワークの紹介
  - effect を足していく
  - effect を handle する

## この発表のゴール

- Cycle.js の存在を覚えて帰ってもらう
- 「Cycle.js おもしろそう，書いてみようかな」と思ってもらう

---

# Web フロントのフレームワークはどのような性質を満たしていてほしいか？

---

# Web フロントのフレームワークの望ましい性質

- 種々の副作用を互いに分離できると嬉しい
- 非同期が織り込み済みだと嬉しい

---

# Web フロントのフレームワークの望ましい性質

- <font color="#ed5565">種々の副作用を互いに分離できると嬉しい</font>
- 非同期が織り込み済みだと嬉しい

---

# 種々の副作用を互いに分離できると嬉しい

アプリケーションは様々な副作用を持つ

- DOM
  - ユーザの入力イベントを受け取る
  - 画面を描画する
- HTTP
  - APIを叩いて結果を受け取る
- ステート管理
- WebSocket
- Location API
- History API
- Web Storage API
- タイトル変えたり……
- favicon 変えたり……
- visibility API とか……

メッチャあるやんけ😰

---

# 種々の副作用を互いに分離できると嬉しい (cont'd)

- 異なる副作用は別々に処理したい
  - あらゆる副作用をゴッチャにしたくない
  - 関心の分離をしたい
  - テスタビリティ
- 後から副作用を追加できるようにしておきたい
  - 拡張性

---

# 種々の副作用を互いに分離できると嬉しい (cont'd)

<b>React + Redux (+ Redux middleware)</b> は
<b>DOM + ステート管理 (+ その他 IO すべて)</b> に見える
(少なくとも私には)

---

# Web フロントのフレームワークの望ましい性質 (cont'd)

- 種々の副作用を互いに分離できると嬉しい
- <font color="#ed5565">非同期が織り込み済みだと嬉しい</font>

---

# 非同期が織り込み済みだと嬉しい

- どうあがいても絶対必要なので
  - API のレスポンスはいつ返ってくるかわからない
  - ユーザの入力に対する高価な処理 (e.g. 検索 API を叩く) の debouncing
- 「どこに何を書けばいいかわからない」は利用者がつらい
  - お前のことやぞ Redux (middleware)

---

# 望ましい性質を満たすには？

---

# 望ましい性質を満たすには？

Web フロントのフレームワークの望ましい性質 (再掲)

- 種々の副作用を互いに分離できると嬉しい
  - → ？？？
- 非同期が織り込み済みだと嬉しい
  - → ？？？

--- 

# 望ましい性質を満たすには？

Web フロントのフレームワークの望ましい性質 (再掲)

- 種々の副作用を互いに分離できると嬉しい
  - → それぞれの副作用を effect 的に扱う
- 非同期が織り込み済みだと嬉しい
  - → Observable / Stream

---

# 望ましい性質を満たすには？

Web フロントのフレームワークの望ましい性質 (再掲)

- 種々の副作用を互いに分離できると嬉しい
  - → <font color="#ed5565">それぞれの副作用を effect 的に扱う</font>
- 非同期が織り込み済みだと嬉しい
  - → Observable / Stream

---

# それぞれの副作用を effect 的に扱う

- 副作用の処理を client-server モデルとして捉える
- client (アプリケーション) は server (effect handler) に副作用 (effect) の処理を依頼し，結果を受け取る

<img src="https://gyazo.com/f26e477c90c250272fd13c93e6041fdc/raw" width="80%">

---

# それぞれの副作用を effect 的に扱う (cont'd)

- 異なる handler は異なる effect をハンドルする
  - 関心事の分離
- アプリケーションが新たな effect を要求するようになれば handler も増やせばよい
  - 拡張性の確保

---

# 望ましい性質を満たすには？ (cont'd)

- 種々の副作用を互いに分離できると嬉しい
  - → それぞれの副作用を effect 的に扱う
- 非同期が織り込み済みだと嬉しい
  - → <font color="#ed5565">Observable / Stream</font>

---

# Observable / Stream

- [ReactiveX](http://reactivex.io/) が代表的
- Cycle.js では [xstream](http://staltz.github.io/xstream/) が主要な選択肢
- 非同期に値が流れてくるやつ
- Observable が値を流し，Observer がその値を使って何をするかを決める
- 一応形式的に考察されていたりするっぽい
  - [A Formal Model for Direct-style Asynchronous Observables (Philipp Haller, Heather Miller)](https://arxiv.org/abs/1511.00511)
- 以下 <i>Stream</i> と呼びます

---

# Stream (cont'd)

例 : ダブルクリックの検出
![](https://camo.githubusercontent.com/995c301de2f566db10748042a5a67cc5d9ac45d9/687474703a2f2f692e696d6775722e636f6d2f484d47574e4f352e706e67)
出典 : [The introduction to Reactive Programming you've been missing
](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)

---

# 望ましい性質を満たすには？ (cont'd)

- 種々の副作用を互いに分離できると嬉しい
  - → それぞれの副作用を effect 的に扱う
- 非同期が織り込み済みだと嬉しい
  - → Observable / Stream

<font color="#ed5565">effect handler と Stream で非同期にやりとりすればいいのでは？</font>

---

# ところで Cycle.js というフレームワークがありまして……

---

# Cycle.js

<img src="https://cycle.js.org/img/cyclejs_logo.svg" width="15%">

- "A functional and reactive JavaScript framework for predictable code"
- [GitHub](https://github.com/cyclejs/cyclejs) では約 9,300 stars
  - 「名前だけは知ってる」という方も多いかも？

---

# Cycle.js (cont'd)

Cycle’s core abstraction is your application as a pure function `main()` where inputs are read effects (*`sources`*) from the external world and outputs (*`sinks`*) are write effects to affect the external world. These I/O effects in the external world are managed by *`drivers`*: plugins that handle DOM effects, HTTP effects, etc.

([cycle.js.org](https://cycle.js.org/) より引用)

---

# これやんけ

---

# 図

[![Screenshot from Gyazo](https://gyazo.com/6bfe054a1a167b0a9e4cffde49c9f785/raw)](https://gyazo.com/6bfe054a1a167b0a9e4cffde49c9f785)

---

# 用語

- <b><i>source</i></b>
  - 外界からのメッセージが流れてくる stream
  - <i>component</i> が <i>driver</i> から受け取る
- <b><i>sink</i></b>
  - 外界に干渉するためのメッセージを流す stream
  - <i>component<i> が <i>driver<i> に渡す
- <b><i>driver</i></b>
  - <i>sink</i> を受け取って (典型的には) 副作用を起こし，<i>source</i> を返す関数
  - e.g. DOM driver, HTTP driver
  - <font color="#ed5565">effect を handle できる</font>
- <b><i>component</i></b>
  - <i>source</i>s を受け取って <i>sink</i>s を返す関数
  - React や Vue の component とは意味が異なるので注意
- <b><i>higher-order component</i></b>
  - *component* を取って *component* を返す関数
  - <font color="#ed5565">effect を handle できる</font>

---

# effect 的な Cycle.js の世界観

- component は effectful な計算である
- component が持つ effect は driver や higher-order component によって handle される
- すべての effect を handle することができればアプリケーションが実行できる

---

# component を書く

---

# effect を足していきます

- 1. `DOM` effect
- 2. `state` effect
- 3. `WebSocket` effect
- 4. `toast` effect

---

# effect を足していきます

- <font color="#ed5565">1. `DOM` effect</font> 🔥
- 2. `state` effect
- 3. `WebSocket` effect
- 4. `toast` effect

---

# 1. `DOM` effect

```types.ts
type SoDOM = { DOM: DOMSource }; // DOM driver から受け取る
type SiDOM = { DOM: Stream<JSX.Element> }; // DOM driver に渡す
```

```tsx:main.tsx
function main({ DOM }: SoDOM): SiDOM {
    const submitEvent$: Stream<Event> = DOM.select('.submit').events('click');
    const bodyInputEvent$: Stream<Event> = DOM.select('.body').events('input');

    const body$: MemoryStream<string> = Stream.merge(
        bodyInputEvent$.map((e: any) => e.target.value),
        submitEvent$.mapTo(''), // 送信されたらクリア
    ).startWith('');

    const view$ = body$.map(body => 
        <div>
            <input type="text" className="body" value={body} />
            <button type="button" className="submit">
                送信
            </button>
            <ul>
                {messages.map(msg => (
                    <li key={msg}>{msg}</li>
                ))}
            </ul>
        </div>
    );

    return {
        DOM: view$,
    };
}
```

---

# 1. `DOM` effect (cont'd)

想像図

[![Screenshot from Gyazo](https://gyazo.com/18bc1c92cd10e00f8d36fbb315bff892/raw)](https://gyazo.com/18bc1c92cd10e00f8d36fbb315bff892)

<b>なぜ「想像」なのか？</b>
→ `DOM` effect がどのように handle されるかで挙動が変わりうるから

---

# effect を足していきます (cont'd)

- 1. `DOM` effect
- <font color="#ed5565">2. `state` effect</font> 🔥
- 3. `WebSocket` effect
- 4. `toast` effect

---

# 2. `state` effect

```State.ts
type State = {
    body: string;
    messages: string[];
}

function State(): State {
    return {
        body: '',
        messages: [],
    };
}
```

```types.ts
export type Endo<T> = (x: T) => T;

export type SoState = { state: StateSource<State> };
export type SiState = { state: Stream<Endo<State>> };
```

```main.ts
export function main({ DOM, state }: SoDOM & SoState): SiDOM & SiState<State> {
    const state$ = state.stream; // 現在の state が得られる

    // ...

    const view$ = state$.map(View); // view は切り出しました

    const reducer$ = Stream.merge<Endo<State>>(
        bodyInputEvent$.map((e: any) => state => ({ ...state, body: e.target.value })),
        submitEvent$.mapTo(state => ({ ...state, body: '' })),
    ).startWith(_ => State());

    return {
        DOM: view$,
        state: reducer$, // Endo<State> を流す
    };
}
```

---

# 2. `state` effect (cont'd)

想像図 (さっきと一緒)

[![Screenshot from Gyazo](https://gyazo.com/18bc1c92cd10e00f8d36fbb315bff892/raw)](https://gyazo.com/18bc1c92cd10e00f8d36fbb315bff892)

---

# effect を足していきます (cont'd)

- 1. `DOM` effect
- 2. `state` effect
- <font color="#ed5565">3. `WebSocket` effect</font> 🔥
- 4. `toast` effect

---

# 3. `WebSocket` effect

```types.ts
type SoWebSocket = { WebSocket: Stream<IncomingMessage> };
type SiWebSocket = { WebSocket: Stream<OutgoingMessage> };
```

```main.ts
function main({
    // ...
    WebSocket,
}: SoDOM & SoState & SoWebSocket): SiDOM & SiState & SiWebSocket {
    // ...

    const outgoingMessage$ = submitEvent$
        .compose(sampleCombine(state$)) // submit 時の state を切り取る
        .map<OutgoingMessage>(([_, { body }]) => ({ type: 'message', body }));

    const reducer$: Stream<Endo<State>> = Stream.merge<Endo<State>>(
        // ...
        WebSocket.filter(IncomingMessage.isMessage).map(({ message }) => state => ({
            ...state,
            messages: [message, ...state.messages], // 受け取ったメッセージを追加する
        })),
    ).startWith(_ => State());

    return {
        // ...
        WebSocket: outgoingMessage$, // client -> server
    };
}
```

---

# 3. `WebSocket` effect (cont'd)

想像図

[![Screenshot from Gyazo](https://gyazo.com/99fc73b5ca68d65ebc8dc537a6a492d6/raw)](https://gyazo.com/99fc73b5ca68d65ebc8dc537a6a492d6)

---

# effect を足していきます (cont'd)

- 1. `DOM` effect
- 2. `state` effect
- 3. `WebSocket` effect
- <font color="#ed5565">4. `toast` effect</font> 🔥

---

# 4. `toast` effect

```types.ts
type SoToast = {};
type SiToast = { toast: Stream<string | null> };
```

```main.tsx
function main({
    // ...
}: SoDOM & SoState & SoToast & SoWebSocket): SiDOM & SiState & SiToast & SiWebSocket {
    // ...

    const toast$ = WebSocket
        .filter(IncomingMessage.isRoomJoin)
        .map(({ name }) => `${name} さんが入室しました`);

    return {
        // ...
        toast: toast$,
    };
}
```

---

# 4. `toast` effect (cont'd)

想像図

[![Screenshot from Gyazo](https://gyazo.com/5cb8e42896e8181c3c0515fe24657b2f/raw)](https://gyazo.com/5cb8e42896e8181c3c0515fe24657b2f)

---

# effect のハンドリング

---

# effect のハンドリング

- component の effect をすべて取り除くことで，アプリケーションを実行することができる
- 取り除いていくぞ

---

# `toast` effect

`DOM` effect に押し付ける

```handleToast.tsx
function handleToast<So extends SoDOM, Si extends Partial<SiDOM & SiToast>>(
    component: Component<So, Si>,
): Component<So, Omit<Si, 'toast'>> {
    return function(sources) {
        const sinks = component(sources);
        const SiDOM: Stream<JSX.Element> = sinks.DOM || Stream.empty();
        const SiToast: Stream<string | null> = sinks.toast || Stream.empty();

        const view$ = Stream.combine(SiDOM, SiToast.startWith(null))
            .map(いい感じにガッチャンコする);

        return {
            ...sinks,
            DOM: view$,
        };
    };
}
```

---

# `state` effect

自己解決 (中で循環参照してる)

```typescript
function withState<
    So extends { state: StateSource<State> },
    Si extends { state: Stream<Endo<State>> }
>(
    main: Component<So, Si>
): Component<Omit<So, 'state'>, Omit<Si, 'state'>>;
```

---

# `DOM` effect & `WebSocket` effect

残りは driver で

```typescript
const drivers = {
    DOM: makeDOMDriver('#app'),
    WebSocket: makeWebSocketDriver(),
};

run(handleToast(withState(main)), drivers);
```

---

# テスト

---

# テスト

- component は sources → sinks なただの関数

```typescript
const sinks = component({
    DOM: mockDOMSource(...), // compatible な sources を与える
    // ...
});

// ここで sinks.DOM や sinks.WebSocket に対して assertion を書くだけ
```

---

# テスト (cont'd)

```typescript
it('emits 42', done => {
    // ...

    foo$.take(1).addListener({
        next(x) {
            expect(x).toBe(42);
        },
        complete() {
            done();
        },
    });
});
```

---

# テスト (cont'd)

好きな組み合わせをテストすればよい

- 前提条件
  - ユーザがボタンをクリックしたときに……
  - API のレスポンスが得られたときに……
  - WebSocket でメッセージを受け取ったときに……
- アサーション
  - API リクエストが飛ぶか？
  - state は更新されるか？
  - WebSocket でメッセージが送られるか？
  - toast は表示されるか？

---

# 終わりに

---

# 終わりに

- Cycle.js はこんなフレームワーク
  - 副作用を effect 的に扱える
  - effect handler (driver) とは Stream でやり取りする
  - higher-order component でも effect を handle できる
  - Web フロント以外にも使えて汎用的
- もし興味があれば書いてみてください！
  - その他気になった方はよければ懇親会で話しかけてください

---

# ご清聴ありがとうございました！