- 投稿日:2019-08-16T22:08:39+09:00
React Hooks の状態を iframe との間で同期したい
例えばユーザーがHTMLを書けるエディタを実装するような時は、セキュリティの都合により描画先を iframe に隔離する必要があります。そういう場合は大概 iframe 内のロジックも複雑になるので、なるべく扱いやすい形で開発したいわけです。
フロントエンドを React で開発しているのであれば、親子フレームそれぞれの特定 Component 間で状態が共有できると良さそうな気がします。
export const Parent: React.FC = () => { const [state, setState] = React.useState({}) // 親の状態と } export const Child: React.FC = () => { const [state, setState] = React.useState({}) // 子の状態を同期したい }単純に
stateを送信しあって同期する戦略は、stateが大きくなると通信やレンダリングのコストが嵩むため避けたいところ。state 全体ではなく更新のための情報を使って同期するのであれば、React Hooks のuseReducerで状態を管理して actions をやり取りするのが素直な方法でしょう。ページロード時にだけ
state全体を同期して、その後はdispatchを呼ぶと他フレーム上でも同じ action でdispatchが実行されるようにする。これでフレーム間の通信を意識しないで開発ができそうです。Component での使い方は次のような感じ。import React from 'react' import { ParentProxy, ChildProxy, useProxyReducer } from './proxy' type State = { greeting: string } type Action = { type: 'sleep' } | { type: 'meet'; name: string } function reducer(state: State, action: Action) { switch (action.type) { case 'sleep': return { greeting: 'Good night!' } case 'meet': return { greeting: `Hi, ${action.name}!` } } } const initialState = { greeting: 'Good morning!' } const parentProxy = new ParentProxy<State, Action>( 'https://iframe-origin', 'child-iframe' // iframe の id ) // 親フレームの Component const Parent: React.FC = () => { const [state, dispatch] = useProxyReducer(parentProxy, reducer, initialState) return ( <div> <p>parent: {state.greeting}</p> <p> <button onClick={() => dispatch({ type: 'sleep' })}>Go to bed</button> </p> <iframe id="child-iframe" title="Child" src="/iframe.html"></iframe> </div> ) } const childProxy = new ChildProxy<State, Action>('https://parent-origin') // 子フレームの Component const Child: React.FC = () => { const [state, dispatch] = useProxyReducer(childProxy, reducer, initialState) return ( <div> <p>child: {state.greeting}</p> <p> <button onClick={() => dispatch({ type: 'meet', name: 'Bob' })}> Say hello to Bob </button> </p> </div> ) }これを実現する実装は次のような感じで。
useProxyReducer内でuseEffectを使ってmessageイベントを listen し、postMessage処理を差し込んだdispatchを返します。proxy.tsimport React from 'react' type Action = { type: string } // 初期状態を同期するためのアクション export type ProxyAction<S> = { type: 'setState'; state: S } // postMessage でやり取りするメッセージ type Message<S, A> = // 親フレーム <-> 子フレーム // 他方で呼ばれたアクションをこちらでも実行する | { type: 'dispatch'; action: A } // 親フレーム <- 子フレーム // 初期状態同期のために state 全体をリクエスト | { type: 'sendState' } // 親フレーム -> 子フレーム // sendState メッセージのレスポンス | { type: 'receiveState'; state: S } abstract class Proxy<S, A extends Action> { // eslint-disable-next-line no-useless-constructor constructor(private targetOrigin: string) {} // メッセージ送信先 window protected abstract getTargetWindow(): Window | null protected abstract handleMessage( message: Message<S, A>, dispatch: (action: A | ProxyAction<S>) => void ): void protected postMessage(message: Message<S, A>) { const target = this.getTargetWindow() if (target) { target.postMessage(message, this.targetOrigin) } } // 接続開始 subscribe(dispatch: (action: A | ProxyAction<S>) => void) { const onMessage = (event: MessageEvent) => { const message = event.data as Message<S, A> if (message.type === 'dispatch') { return dispatch(message.action) } else { this.handleMessage(message, dispatch) } } window.addEventListener('message', onMessage) return () => { window.removeEventListener('message', onMessage) } } // dispatch した後にこれを呼ぶと他フレームでも実行される proxyDispatch(action: A) { this.postMessage({ type: 'dispatch', action }) } // eslint-disable-next-line @typescript-eslint/no-unused-vars setState(state: S) {} } // 親フレーム用プロキシ export class ParentProxy<S, A extends Action> extends Proxy<S, A> { // 子フレームから同期のために要求された時に送る state。 private state?: S // state が更新されたときは常に呼ぶ setState(state: S) { this.state = state } constructor(targetOrigin: string, private iframeId: string) { super(targetOrigin) } protected getTargetWindow() { const iframe = window.document.getElementById(this.iframeId) if (!iframe) return null return (iframe as HTMLIFrameElement).contentWindow } protected handleMessage(message: Message<S, A>) { switch (message.type) { case 'sendState': // 子フレームからの初期状態要求に答える this.sendState() } } subscribe(dispatch: (action: A | ProxyAction<S>) => void) { const unsubscribe = super.subscribe(dispatch) // 接続時に子フレームに同期させる this.sendState() return unsubscribe } // 子フレームに状態を送る private async sendState() { this.postMessage({ type: 'receiveState', state: await this.getState() }) } // subscribe 時には state がセットされていない可能性があるため、セットされるまで待つ private async getState(): Promise<S> { return new Promise(resolve => { const wait = () => { if (this.state) { return resolve(this.state) } setTimeout(wait, 100) } wait() }) } } export class ChildProxy<S, A extends Action> extends Proxy<S, A> { protected getTargetWindow() { if (window.parent === window.self) return null return window.parent } protected handleMessage( message: Message<S, A>, dispatch: (action: A | ProxyAction<S>) => void ) { switch (message.type) { case 'receiveState': // 親から state が帰ってきたので同期する dispatch({ type: 'setState', state: message.state }) } } subscribe(dispatch: (action: A | ProxyAction<S>) => void) { const unsubscribe = super.subscribe(dispatch) // 接続時に同期要求 this.postMessage({ type: 'sendState' }) return unsubscribe } } export function useProxyReducer<S, A extends Action>( proxy: Proxy<S, A>, reducer: (state: S, action: A) => S, initialState: S ): [S, (action: A) => void] { function proxyReducer(state: S, action: A | ProxyAction<S>): S { // 同期 action 処理を追加 if (action.type === 'setState') { return (action as ProxyAction<S>).state } return reducer(state, action as A) } const [state, dispatch] = React.useReducer(proxyReducer, initialState) // 接続 React.useEffect(() => { return proxy.subscribe(dispatch) }, [proxy, dispatch]) // state の変更を反映(直接 state を同期するのは接続時のみ) React.useEffect(() => { proxy.setState(state) }, [proxy, state]) return [ state, action => { dispatch(action) proxy.proxyDispatch(action) } ] }
- 投稿日:2019-08-16T22:08:39+09:00
React Hooks の状態を iframe 内の Component との間で同期したい
例えばユーザーがHTMLを書けるエディタを実装するような時は、セキュリティの都合により描画先を iframe に隔離する必要があります。そういう場合は大概 iframe 内のロジックも複雑になるので、なるべく扱いやすい形で開発したいわけです。
フロントエンドを React で開発しているのであれば、親子フレームそれぞれの特定 Component 間で状態が共有できると良さそうな気がします。
export const Parent: React.FC = () => { const [state, setState] = React.useState({}) // 親の状態と } export const Child: React.FC = () => { const [state, setState] = React.useState({}) // 子の状態を同期したい }単純に
stateを送信しあって同期する戦略は、stateが大きくなると通信やレンダリングのコストが嵩むため避けたいところ。state 全体ではなく更新のための情報を使って同期するのであれば、React Hooks のuseReducerで状態を管理して actions をやり取りするのが素直な方法でしょう。ページロード時にだけ
state全体を同期して、その後はdispatchを呼ぶと他フレーム上でも同じ action でdispatchが実行されるようにする。これでフレーム間の通信を意識しないで開発ができそうです。Component での使い方は次のような感じ。import React from 'react' import { useProxyReducer } from './proxy' type State = { greeting: string } type Action = { type: 'sleep' } | { type: 'meet'; name: string } function reducer(state: State, action: Action) { switch (action.type) { case 'sleep': return { greeting: 'Good night!' } case 'meet': return { greeting: `Hi, ${action.name}!` } } } const initialState = { greeting: 'Good morning!' } const parentProxy = { master: true, targetOrigin: '*', targetWindow: () => { const iframe = window.document.getElementById('child-iframe') if (!iframe) return null return (iframe as HTMLIFrameElement).contentWindow } } // 親フレームの Component const Parent: React.FC = () => { const [state, dispatch] = useProxyReducer(parentProxy, reducer, initialState) return ( <div> <p>parent: {state.greeting}</p> <p> <button onClick={() => dispatch({ type: 'sleep' })}>Go to bed</button> </p> <iframe id="child-iframe" title="Child" src="/iframe.html"></iframe> </div> ) } const childProxy = { targetOrigin: '*', targetWindow: () => window.parent } // 子フレームの Component const Child: React.FC = () => { const [state, dispatch] = useProxyReducer(childProxy, reducer, initialState) return ( <div> <p>child: {state.greeting}</p> <p> <button onClick={() => dispatch({ type: 'meet', name: 'Bob' })}> Say hello to Bob </button> </p> </div> ) }これを実現する実装は次のような感じで。
useProxyReducer内でuseEffectを使ってmessageイベントを listen し、postMessage処理を差し込んだdispatchを返します。proxy.tsimport React from 'react' type Proxy = { master?: boolean // 接続時、master 側の state を他フレームにコピーする targetOrigin: string targetWindow: () => Window | null } type Action = { type: string } // 初期状態を同期するためのアクション export type ProxyAction<S> = { type: 'setState'; state: S } // postMessage でやり取りするメッセージ type Message<S, A> = // master <-> slave // 他方で呼ばれたアクションをこちらでも実行する | { type: 'dispatch'; action: A } // master <- slave // 同期のために state 全体をリクエスト | { type: 'sendState' } // master -> slave // sendState メッセージのレスポンス | { type: 'receiveState'; state: S } function postMessage<S, A>(proxy: Proxy, message: Message<S, A>) { const target = proxy.targetWindow() if (target) { target.postMessage(message, proxy.targetOrigin) } } // 接続 function subscribe<A, S>( proxy: Proxy, dispatch: (action: A | ProxyAction<S>) => void, sync: () => void ) { const onMessage = (event: MessageEvent) => { const message = event.data as Message<S, A> switch (message.type) { case 'dispatch': return dispatch(message.action) case 'sendState': return sync() case 'receiveState': return dispatch({ type: 'setState', state: message.state }) } } window.addEventListener('message', onMessage) if (proxy.master) { sync() } else { postMessage(proxy, { type: 'sendState' }) } return () => { window.removeEventListener('message', onMessage) } } // master から slave に state をコピーする function sync<S>(proxy: Proxy, state: S) { postMessage(proxy, { type: 'receiveState', state }) } function proxyDispatch<A>(proxy: Proxy, action: A) { postMessage(proxy, { type: 'dispatch', action }) } export function useProxyReducer<S, A extends Action>( proxy: Proxy, reducer: (state: S, action: A) => S, initialState: S ): [S, (action: A) => void] { function proxyReducer(state: S, action: A | ProxyAction<S>): S { // 同期 action 処理を追加 if (action.type === 'setState') { return (action as ProxyAction<S>).state } return reducer(state, action as A) } const [state, dispatch] = React.useReducer(proxyReducer, initialState) // master 側の needSync を true にすると同期を実行する const [needSync, setNeedSync] = React.useState(false) React.useEffect(() => { return subscribe(proxy, dispatch, () => setNeedSync(true)) }, [dispatch]) // eslint-disable-line react-hooks/exhaustive-deps React.useEffect(() => { if (needSync) { sync(proxy, state) setNeedSync(false) } }, [needSync]) // eslint-disable-line react-hooks/exhaustive-deps return [ state, action => { dispatch(action) proxyDispatch(proxy, action) } ] }React Hooks を使ってスッキリ書けましたというのはまあ間違っていないんですが、状態を
useStateに押し込めてやりくりするあたり頭の体操めいた感が無きにしも非ず。
- 投稿日:2019-08-16T16:47:30+09:00
【React + Redux + Firebase Authentication】Promises must be handled appropriately のエラー対応
事象
以下のコードにおいて、
Promises must be handled appropriatelyのエラー文にぶち当たりました。
src/containers/Auth.tsx// 中略 const mapStateToProps = (state: StateProps) => ({ isAuth: state.isAuth }) const mapDispatchToProps = (dispatch: DispatchProps) => { return { dologin: () => { let provider = new firebase.auth.GoogleAuthProvider() firebase.auth().signInWithPopup(provider) }, refLogin: () => { firebase.auth().onAuthStateChanged(user => { if (!user) { return } dispatch(loginOk(user)) }) } } } // 中略解決策
以下のコードを、
firebase.auth().signInWithPopup(provider)の下に追加してあげます。.then(user => { console.log(user) }) .catch(error => { console.log(error) })こちらが追加後のコードです。
src/containers/Auth.tsx// 中略 const mapStateToProps = (state: StateProps) => ({ isAuth: state.isAuth }) const mapDispatchToProps = (dispatch: DispatchProps) => { return { dologin: () => { let provider = new firebase.auth.GoogleAuthProvider() firebase .auth() .signInWithPopup(provider) .then(user => { console.log(user) }) .catch(error => { console.log(error) }) }, refLogin: () => { firebase.auth().onAuthStateChanged(user => { if (!user) { return } dispatch(loginOk(user)) }) } } } // 中略これによってエラーが解消されました。
少しでも何かの参考になれば幸いです!
- 投稿日:2019-08-16T16:47:30+09:00
【Redux + Firebase Authentication】Promises must be handled appropriately のエラー対応
事象
以下のコードにおいて、
Promises must be handled appropriatelyのエラー文にぶち当たりました。
src/containers/Auth.tsx// 中略 const mapStateToProps = (state: StateProps) => ({ isAuth: state.isAuth }) const mapDispatchToProps = (dispatch: DispatchProps) => { return { dologin: () => { let provider = new firebase.auth.GoogleAuthProvider() firebase.auth().signInWithPopup(provider) }, refLogin: () => { firebase.auth().onAuthStateChanged(user => { if (!user) { return } dispatch(loginOk(user)) }) } } } // 中略解決策
以下のコードを、
firebase.auth().signInWithPopup(provider)の下に追加してあげます。.then(user => { console.log(user) }) .catch(error => { console.log(error) })こちらが追加後のコードです。
src/containers/Auth.tsx// 中略 const mapStateToProps = (state: StateProps) => ({ isAuth: state.isAuth }) const mapDispatchToProps = (dispatch: DispatchProps) => { return { dologin: () => { let provider = new firebase.auth.GoogleAuthProvider() firebase .auth() .signInWithPopup(provider) .then(user => { console.log(user) }) .catch(error => { console.log(error) }) }, refLogin: () => { firebase.auth().onAuthStateChanged(user => { if (!user) { return } dispatch(loginOk(user)) }) } } } // 中略これによってエラーが解消されました。
少しでも何かの参考になれば幸いです!
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社(2019/08/28)
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takashi.age = 37; // ageプロパティを上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 } -- 上書きでなく新しいレコードが生成されます。レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということはできません。
上記のコードも、一部を変えた新しいレコードが作り出されます。
元のレコード(takashi)は36歳のまま残ります。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、例えば
console.logをtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。
Elmでは、場面や状態の変化を直接コードで書けない
再代入という概念がなく、コード内の全ての値が不変だからです。
Elmseason = "夏" -- ずーっと夏 age = 36 -- ずーっと36歳そのため「この行でageを呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。
再代入できないことによるメリット
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言します。
JavaScriptfunction add (a, b) { return a + b; }また、無名関数を変数に格納するパターンもあります。
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書く場合は↓こうです。
JavaScriptconst add = (a, b) => a + b;実行するときは↓こうですね。
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を使わない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を受け取って利用しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして戻り値を使わない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」という定義をするような書き方がメインになります。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
デモアプリ
ブラウザエディタEllieでご確認ください。
※好きにコードをいじってもらって大丈夫です!
(私のコードとは別に保存されるので)
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界のモノを認識するときの考え方に似ているため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。
Elmでも、オブジェクト指向っぽく考えることもできる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。ElmaddAge : Int -> Human -> Human -- 型注釈 addAge int human = { human | age = human.age + int }Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divやbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。ElmbuttonComponent props = button [ class "common-btn" ] [ text props.text ]これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。そして、環境構築のコストもかなり低めです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!とエラーが出るエラーメッセージが分かりやすいのもElmの特徴です。特に、機能追加をしている時などは「そこにコードを追加するなら、ここにも追加しないとでは!?」と導いてくれているような感じがします。
エラーになるようなコードはコンパイル時に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。再代入によって値を変えることで状態変化を表現することはできないため、状態を変化させたい値は
modelに組み込むことになります。
そして、状態を更新する処理はupdate関数の中に書いていきます。
コードの記述方法がある程度定まっていることで「この処理は、この辺りに書いてあるだろう」と予測しやすくなるというメリットがあります。
参照透過性が担保されている
Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。
例えば、期限付きのタスク管理システム
↑のテストをする場合のことを考えてみます。
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
これだとテストしづらいですよね。。。
条件は全て引数として渡す
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。
テストの自動化
例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。
そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。
Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux + TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
もちろん、複雑なものを作ろうとすればそれなりに難しいんですが、少し勉強したらピンポンゲームやシングルページアプリケーションをザコーダーの私でも作ることができました。
まだまだ紹介しきれていない魅力が本当にたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済むので、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takahsi.age = 37; // ageプロパティ上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 }レコードも完全にイミュータブル(不変)なため、ageだけ上書きすることはできません。
一部を変えた新しいレコードを作り出します。
場面や状態の変化を直接コードで書けない
再代入できないからです!
Elmseason = "夏" -- ずーっと夏 takashi_age = 36 -- ずーっと36歳
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。
JavaScriptfunction add (a, b) { return a + b; }または
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書くと
JavaScriptconst add = (a, b) => a + b;実行するときは
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、
takashi_age = 〇〇;の上にあるか下にあるかが重要です。
Elmでは状態変化を直接コードで表現できない
再代入ができないため「この値を、この行で呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。
再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
そのため、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行でも呼び出すことができます。
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を変数なり定数に格納しないと意味がないのです。
唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
Elm年齢カウンター
ブラウザエディタEllieでご確認ください。
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこからオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38コード的にも関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然です。
Elmでも、オブジェクト指向っぽくも捉えられる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divとかbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!と導いてくれるエラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。
何かを変える処理はupdate関数の中で集中管理します。
参照透過性が担保されている
関数は同じ引数なら同じ戻り値を返す(外部の値に依存して振る舞いを変えない)ので、単体テスト・自動テストがしやすいです。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。例えば期限付きのタスク管理システム
のテストをする場合に・・・
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
なんてことになりますね。
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、テストが書きやすくなる。
TDDにも向いています。
テストをゴリゴリに書いておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、テストでエラーが出て気づけるので安心ですよね。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社(8/28)
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takahsi.age = 37; // ageプロパティ上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 }レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということもできません。
上記のコードも、一部を変えた新しいレコードを作り出しています。
元のレコード(takashi)は36歳のままです。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、例えば
console.logをtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。
Elmでは、場面や状態の変化を直接コードで書けない
再代入という概念がなく、コード内の全ての値が不変だからです。
Elmseason = "夏" -- ずーっと夏 age = 36 -- ずーっと36歳そのため「この行でageを呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。
再代入できないことによるメリット
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。
JavaScriptfunction add (a, b) { return a + b; }または
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書くと
JavaScriptconst add = (a, b) => a + b;実行するときは
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を返して変数なり定数に格納しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
デモアプリ
ブラウザエディタEllieでご確認ください。
※好きにコードをいじってもらって大丈夫です!
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。
Elmでも、オブジェクト指向っぽく考えることもできる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divやbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!と導いてくれるエラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。そのため、状態変化を表現したい値は
modelに組み込まなければなりません。また、状態を更新する処理はupdate関数の中に書かなければいけません。この縛りは一見窮屈なようですが、ルールが明確化されていることで、結果的にそれぞれの処理を探しやすくなるというメリットがあります。
参照透過性が担保されている
Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。
例えば、期限付きのタスク管理システム
↑のテストをする場合のことを考えてみます。
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
これだとテストしづらいですよね。
条件は全て引数として渡す
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。
テストの自動化
例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。
そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。
Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T11:14:09+09:00
React Hooks と Context で手軽に状態管理がしたい
例えばダッシュボードだとか複雑なフォームだとかで局所的に Flux 的な状態管理を行いたい状況はよくあります。でもそのために Redux なりを使うのは気が重い……ということで React Hooks で作成した状態を Context で伝播させて穏便に事を済ませることにします。お手軽かつ使いやすい扱い方を検討したい。
React.useState
最もプリミティブな形の実装。親 Component で作成した
stateとそのセッターを Context を通じて子 Component に渡す。import React from 'react' type State = { greeting: string } const initialState = { greeting: 'Good morning' } const StateContext = React.createContext<[State, (state: State) => void]>([ initialState, () => {} ]) const Parent: React.FC = () => { const [state, setState] = React.useState<State>(initialState) return ( <StateContext.Provider value={[state, setState]}> <Child /> </StateContext.Provider> ) } const Child: React.FC = () => { const [state, setState] = React.useContext(StateContext) function changeGreeting() { setState({ greeting: 'Good night!' }) } return <div onClick={changeGreeting}>{state.greeting}</div> }React.useReducer
先の例では各々の子 Component が
setStateを呼ぶのですが更新のロジックは1箇所にまとめたい、stateを直接触るのもなー、などもう少し構造が欲しい感があります。そういうわけで React が提供している Hooks API が useReducer です。https://reactjs.org/docs/hooks-reference.html#usereducer
リファレンスページに載っている例
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); }い、いやじゃ…… action の定義や reducer の switch 文など書きとうない……。また reducer では async な処理が書けないので、そのためにもう1層噛ませることになります。ここら辺の再発明をするのであれば Redux 使えばいいんじゃないかなという気がします。
結局どうするか
子 Component から状態にアクセスする時は
stateとsetStateを wrap した関数を使う、程度でいいんじゃないでしょうか。function useGreeting() { const [state, setState] = React.useContext(StateContext) return { greeting: state.greeting, changeGreeting() { setState({ greeting: 'Good night!' }) } } } const Child: React.FC = () => { const { greeting, changeGreeting } = useGreeting() return <div onClick={changeGreeting}>{greeting}</div> }
- 投稿日:2019-08-16T08:43:27+09:00
react で "number of file watcher reached" になり yarn start できなかった (ubuntu 18.04)
ubuntu18.04でreacを使い yarn startを実行しようとした所以下のエラーになった
Error: ENOSPC: System limit for number of file watchers reached原因
system's file watchersの上限を超えていた
対策
以下を実行してファイル監視数の上限を開放する。
$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p参考
https://github.com/gatsbyjs/gatsby/issues/11406
https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers#the-technical-details
- 投稿日:2019-08-16T01:55:39+09:00
JavaScript/Reactの見直し用記事
はじめに
いろんな言語をやっているとどうしても忘れてしまうのでよく忘れるものをメモ
以下の違いって何?って質問された時微妙な答えをしてしまったのでコレもメモ
フロントエンドとは?
- HTMLの構築
- バックエンドへAPIを呼び出す
- データの返却を受け取るバックエンドとは?
- データベースとの接続
- フロントエンドからの要求に対応するデータを返すAPIを用意
- データをフロントエンドへ渡すDOMとは
「Document Object Model」の略。HTMLとJavaScriptをつなぐ役割をもつ。
階層構造を取り、各要素を表すノードで表現されることが多い。Node.jsとは
JavaScript アプリケーションのプラットフォームでサーバーサイドのJavaScript
ReactなどのJSフレームワークを導入する際にはNode.jsが必要となる。(Node.jsが支える側に)
Node.jsを使わずにReactを利用するには、CDN経由で読み込む。node.jsのバージョン管理はnodebrewを使う。これはhomebrewでインストールする。
homebrew自体でもでもnodeは管理できるがおすすめされない。npmとは
「Node Package Manager」の略。Node.jsのパッケージを管理するツールである。
nodebrewからNode.jsをインストールすると付いてくる。yarnとは
npmと同様にJavaScriptのパッケージ管理ツール。
高速で、信頼性が高く、安全性も高い。
homebrewからインストールする。JavaScript
1個目の書き方(関数式)
関数を宣言する際に関数名を記述しなくても良いので、無名関数や匿名関数とも呼ばれる方法
関数式const 定数名 = function(仮引数, 仮引数, ...) { 処理; return 返り値; };関数式let 変数名 = function(仮引数, 仮引数, ...) { 処理; return 返り値; };呼び出し定数名(実引数, 実引数, ...);例const name = function(x) { console.log(x) // 出力: 香風智乃 }; name("香風智乃")2個目の書き方(関数宣言)
関数をそのまま宣言することでプログラム内で利用することができるようにする方法
関数宣言function 関数名(仮引数, 仮引数, ...) { 処理; return 返り値; }呼び出し関数名(実引数, 実引数, ...);例function dispMsg(str){ console.log(str); // 出力: 保登心愛 } dispMsg("保登心愛");3個目の書き方(アロー関数)
ES2015(ES6)から導入され、
functionを使わない代わりに、=>で関数を表現することができるアロー関数const 定数名 = (仮引数, 仮引数, ...) => { 処理; return 返り値; }アロー関数let 変数名 = (仮引数, 仮引数, ...) => { 処理; return 返り値; }呼び出し定数名(実引数, 実引数, ...);例const name = (x) => { console.log(x) } name("香風智乃")参考記事
JavaScriptのthisの理解
アロー関数式
prototype
Object
method
コールバック関数
非同期処理
テンプレートリテラル
React
create-react-app$ npx create-react-app アプリ名 $ cd アプリ名 $ yarn startフォーム
なぜコールバック関数を使用する際はアロー関数を使わないとエラーになるのか
React-Redux
- Quick Start(React Redux 公式サイト)
- React-Reduxの動きがよくわからなくなるのでまとめた
- React + Reduxで作ったIsomorphic(Universal) JSなサービス開発
非同期処理
React Hooks
- 投稿日:2019-08-16T01:24:30+09:00
NextJSとMaterialUIでパンくずリストを作ってみる
NextJSとMaterialUIでパンくずリストを作ってみる
なかなかパンくずリストの作成って意外と面倒くさいですよね。
特にNextJSやNuxtJSだとSSRがあったりしてなかなか辛かったりもします。
日本語の記事だとNuxtJS関連のものはよく出てくるのですがNextJS関連のものはあまり出てこなくて
(´・ω・`)となったりします。
しかもNextJS9になってからはダイナミックルーティングが追加されたりとなかなかやりたいことができそうになってきそうです。
そこで今回はNextJS + MaterialUIというテーマでやってみたので忘備録です。目指したいパンくずリストは次のようなイメージです。
/users/1/orders/1=>ユーザー一覧/ユーザー詳細/注文一覧/注文詳細
ルーティング名がパンくずリストになるような感じです。
ユーザー詳細が実際のユーザーの名前になる実装は実は今回は考慮していません。。。実際のイメージはMaterialUIの公式を見ていただければイメージが付くかと思います
https://material-ui.com/components/breadcrumbs/使ったものたち
- NextJS(ver9)
- MaterialUI
- TypeScript
- yarn
などなど...
必要な依存関係は
yarn addしたものがあるので参考に$ yarn add \ @types/node \ @material-ui/core \ next \ react @types/react react-dom \ typescript --network-timeout 1000000 # materialUIが重いのでタイムアウトを伸ばしたページルーティング
パンくずリストをつくっていく上で重要になってくるのが
URLのパスです。
RESTっぽいとパンくずリスト向きなURLになります、ある程度秩序を持ったルーティングでも作りやすいと思います。(無秩序だと厳しいのでは...)
今回はREST前提な感じのルーティングで作っていきます。
以下のルーティングでパンくずリストを作ろうと思います。
ルーティング 画面名 / トップ画面 /users ユーザー一覧画面 /users/[userId] ユーザー詳細画面 /users/[userId]/orders 注文一覧画面 /users/[userId]/orders/[orderId] 注文詳細画面 /items 商品一覧画面 /items/[itemId] 商品詳細画面 /items/[itemId]/edit 商品編集画面 /about このサイトについて画面? ちなみに基本はRESTっぽい感じですが
/aboutはRESTっぽくなくしています。
NextJSでダイナミックルーティングするときのパスパラメータは[xxx]や[xxxId]と表す(最初ビックリした)のでそれによってpagesにディレクトリとファイルをほります。pages ├── about.tsx ├── index.tsx ├── items │ ├── [itemId] │ │ ├── edit.tsx │ │ └── index.tsx │ └── index.tsx └── users ├── [userId] │ ├── index.tsx │ └── orders │ ├── [orderId] │ │ └── index.tsx │ └── index.tsx └── index.tsxファイルをほったあとは適当にhello world的なものを出力させるようにします。
このときLayoutコンポーネントあたりを作ってラップさせれば後の実装が楽になります。
withRouterコンポーネントについて実装方法は色々あると思うのですが今回は
withRouterというNextJSにバンドルされているHOCを利用した実装方法です。
withRouter自体の利用方法はこんな感じです。const MyComponent = props => { console.log(props.router); ... }; export default withRouter(MyComponent); // ラップ
withRouterを適用するとpropsに次のような内容のものが付与されます。{ router: { pathname: '/users/[userId]/orders/[orderId]', route: '/users/[userId]/orders/[orderId]', query: { userId: '1', orderId: '1' }, asPath: '/users/1/orders/1', events: { on: [Function: on], off: [Function: off], emit: [Function: emit] }, push: [Function], replace: [Function], reload: [Function], back: [Function], prefetch: [Function], beforePopState: [Function] } }これを利用してパンくずリストを作っていきましょう。
実装例
実際に実装していきます。
ポイントはwithRouterから付与されるpathnameとasPathというパラメータです。
これらのパラメータは現在のページのルーティングやパスを表しています。
加えて、これらのパラメータは対応しているため/でSplitしたあと同じIndexでアクセスすればそのセクションが取れるはずです。
そして、それらのパラメータとその前半をくっつければルーティングのいずれかにマッピングできるはずなので、そのルーティングの名前をどこかに定義しマッピングすれば実装が完了します。つまり
pathname=/users/[userId]/orders/[orderId]とasPath=/users/1/orders/1でイメージしてみるとこんな感じになります。const routingMapping = { // ルーティング名の定義を行う '/': { title: 'トップページ' }, '/users': { title: 'ユーザー一覧' }, '/users/[userId]': { title: 'ユーザー詳細' }, '/users/[userId]/orders': { title: '注文一覧' }, '/users/[userId]/orders/[orderId]': { title: '注文詳細' }, } const pathname = '/users/[userId]/orders/[orderId]'.split('/').filter(i => i.length > 0); const asPath = '/users/1/orders/1'.split('/').filter(i => i.length > 0); // 上記の値は次のような感じになる // pathname = ['users', '[userId]', 'orders', '[orderId]'] // asPath = ['users', '1', [orders], '1'] const links = []; let pathnameHierarchy = '/'; // ループさせ `/` => `/users` => `/users/[userId]` => `/users/[userId]/...` と増えていく let asPathHierarchy = '/'; // ループさせ `/` => `/users` => `/users/1` => `/users/1/...` と増えていく const length = pathname.length; for (let i = 0; i < length; i += 1) { const target = mapping[pathHierarchy]; // ルーティング名の定義を取得 links.push( target ? ( <Link color="inherit" className={classes.link} href={asPathHierarchy // hrefで実際のパスの値をリンク } key={asPathHierarchy} > {target.title // 定義したルーティング名のタイトルを注入 } </Link> ) : ( <Link color="inherit" className={classes.link} href={asPathHierarchy} key={asPathHierarchy} > {asPath[i] // ルーティング名が未定義の場合は実際の値を注入 } </Link> ), ); // ループでパスのお尻を増やす部分 pathHierarchy += pathHierarchy.endsWith('/') ? pathname[i] : `/${pathname[i]}`; // 頭に`/`がついた状態で始まるのでパス名が`//users/...`となってしまう対策 asPathHierarchy += asPathHierarchy.endsWith('/') ? asPath[i] : `/${asPath[i]}`; }このままだと最下層が
linksに入りませんがあえてそうしています。
実際にコンポーネントを作成するときに活用されます(変数名deepestを定義しているあたりです)。上記を踏まえてパンくずリストコンポーネントを作ってみます。
import React from 'react'; import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; import { Paper, Breadcrumbs, Typography, Link, } from '@material-ui/core'; import { withRouter } from 'next/router'; const routingMapping = { '/': { title: 'トップページ' }, '/users': { title: 'ユーザー一覧' }, '/users/[userId]': { title: 'ユーザー詳細' }, '/users/[userId]/orders': { title: '注文一覧' }, '/users/[userId]/orders/[orderId]': { title: '注文詳細' }, '/items': { title: '商品一覧' }, '/items/[itemId]': { title: '商品詳細' }, '/items/[itemId]/edit': { title: '商品編集' }, '/about': { title: 'このサイトについて' }, }; const useStyles = makeStyles((theme: Theme) => createStyles({ link: { display: 'flex', }, icon: { marginRight: theme.spacing(0.5), width: 20, height: 20, }, }), ); const Breadcrumbs = props => { const classes = useStyles(props); const pathname = props.router.pathname .split('/') .filter(element => element.length > 0); const asPath = props.router.asPath .split('/') .filter(element => element.length > 0); const length = pathname.length; const links = []; let pathnameHierarchy = '/'; let asPathHierarchy = '/'; for (let i = 0; i < length; i += 1) { const target = routingMapping[pathnameHierarchy]; links.push( target ? ( <Link color="inherit" className={classes.link} href={asPathHierarchy} key={asPathHierarchy} > {target.title} </Link> ) : ( <Link color="inherit" className={classes.link} href={asPathHierarchy} key={asPathHierarchy} > {asPath[i]} </Link> ), ); pathnameHierarchy += pathnameHierarchy.endsWith('/') ? pathname[i] : `/${pathname[i]}`; asPathHierarchy += asPathHierarchy.endsWith('/') ? asPath[i] : `/${asPath[i]}`; } const deepest = mapping[pathHierarchy]; // 最下層=現在のルーティングはクリックできないように<Typography>でリストを作成 links.push( deepest ? ( <Typography color="textPrimary" className={classes.link} key={asPathHierarchy} > {deepest.title} </Typography> ) : ( <Typography color="textPrimary" key={asPathHierarchy}> {asPathHierarchy} </Typography> ), ); return ( <Paper elevation={0} > <Breadcrumbs separator="/" aria-label="breadcrumb"> {links.map(link => (link))} </Breadcrumbs> </Paper> ); }; export default withRouter(Breadcrumbs);あとはこれをLayoutコンポーネントあたりに付与すれば...完成です。
import React from 'react'; import ./Breadcrumbs const Layout = props => { return ( <main> <Breadcrumbs /> { // パンくずリストを挿入! } {props.children} </main> ); }; export default Layout;import React from 'react'; import Layout from '../components/Layout'; const Index = props => { return ( <Layout> <h1>Hello</h1> </Layout> ) } export default Index;デザイン周りはMaterialUIの公式から拝借しました。
所感
今回はNextJSを利用したパンくずリスト作成でしたが、この考え方を応用すれば基本的にどのようなフレームワークのパンくずリストでもページの個別実装なしに作成可能な気がします。
パンくずリスト作成に必要なものはその画面へのルーティングと実際のパスだというのがわかったのは今回大きな収穫です。
やってみると面倒なパンくずリスト作成ですが、これで楽になりたいと思います。
あと、ReactやSSRってwebpack周りで敷居が高そうなイメージだったのですがそのへんをNextJSがうまくやってくれているみたいだったのでだいぶ敷居が下がったようにも感じます。
本業はバックエンドですがフロントも楽しい・ω・