『プログラミングの基礎』を読んだ

ゴールデンウィークを使って読み進めて、ようやく読み終わった。

浅井健一『プログラミングの基礎』サイエンス社

タイトル通り、プログラミングの基礎を学んでいく。言語はOCaml
リストや木構造などの簡単なデータ構造やアルゴリズムについても学んでいく。

Gitを使って内容をまとめながら読んだ。

赤黒木の挿入の説明がよく分からなかったのと、最後のヒープについては飽きてしまったので、やってない。
それ以外は一通りやった。

読もうと思ったキッカケとなったのは、この記事。

19: 読んでよかった技術書 – kdxu – Medium

憧れているプログラマが「プログラミングの作法の9割はこの本から学んだ」と言っていたので、興味を持った。

OCamlというのもいいなと思った。
プログラミングが上手くなるためには関数型言語をやるといいとどこかで読んだし、今は型の強い言語が人気だし。
JavaScriptに型を導入するためのライブラリであるFlowも、OCamlで出来ているし。

そんな訳で読んでみたのだが、非常に勉強になった。

実際に手を動かしていくのがいい。
ステップバイステップで作っていく。いま学んでいる機能やデータ構造をどのように使えばいいのか、どういう時に役に立つのかを示しながら進んでいくから、納得して進みやすい。

よりよいプログラムを書くための考え方や方法論である「デザインレシピ」もよかった。
最初は冗長に感じたが、この方法論に従って書いていくことで、再帰などの抽象度が高めの関数をスムーズに作れるようになる。

特に、再帰をスムーズに書けたのが嬉しかった。
再帰という概念はもちろん知っていたけど、なんとなく苦手意識があった。JavaScriptmapfilterも、再帰というより、配列を舐めていくだけという感じだし。
だが本書で、再帰に対する苦手意識がだいぶ和らいだ。
自然数再帰的なデータ構造と言える」といった話も面白かった。

JavaScript以外の言語をちゃんと触ったのはこれが初めてだが、いかにJavaScriptが自分の手に馴染んでいるか、悪く言えば手癖で書いているかを自覚した。
JavaScriptの場合、自分なりの勘が働く。なんとなく、こうしたほうがいいな、この書き方はまずいな、みたいなものを感じ取れる。その妥当さや精度はともかく、多少なりとも直観が働く。思考が自然に進んでいく、というのだろうか。
だがOCamlの場合、当然ながらそうは行かなかった。「えーと」という感じで意識的に順序立てて考える必要がある。それでもよく分からなかったりする。
単に言語の知識が足りないからというのもあるが、やっぱり慣れというのは重要だなと感じた。

OCamlは面白かったから、今後も勉強してみたいとは思う。
とはいえ、もう少し「実用性が高い(仕事で使いやすい)」言語を勉強したいから、そっちが優先になりそうではある。Go言語とか。
ずっとバックエンドを勉強したいと言っているのだから、そろそろ実践しないと……。

Redux Middleware の仕組みと作り方

Redux の機能を拡張し、非同期処理などを書きやすくする仕組みである Redux Middleware について書いていく。

この記事で使っているライブラリのバージョンは以下。

  • redux@3.7.2
  • react-redux@5.0.7
  • react@16.2.0
  • react-dom@16.2.0

Redux の基本については、以前書いた。
numb86-tech.hatenablog.com

最近読んだこの記事も分かりやすかった。
Vanilla JSで学ぶRedux - Qiita

reducer はプレーンなオブジェクトのみを受け付ける

Middleware の役割を理解するためにはまず、 Redux の仕組みを理解しないといけない。
dispatchを使ってactionreducerに渡し、そのreducerが渡されたactionの内容に応じて新しくstateを生成し、それをstoreが記録する。
というのが、Reduxの基本的な流れである。

そしてactionは、typeプロパティを持ったプレーンなオブジェクトでなければならない。
ここでいう「プレーンなオブジェクト」は、Redux内部のisPlainObjectという関数で判定している。

ここで問題になるのが、promiseオブジェクト。
isPlainObjectでエラーになる。
そのため、APIなどの非同期処理の結果をreducerに渡そうとすると、エラーになってしまう。

import {createStore} from 'redux';

const ADD_COUNTER = 'ADD_COUNTER';

const addCounter = value =>
  Promise.resolve(value).then(res => ({type: ADD_COUNTER, value: res}));

const myReducer = (state = 0, action) => {
  const {type, value} = action;
  switch (type) {
    case ADD_COUNTER:
      return state + value;
    default:
      return state;
  }
};

const myStore = createStore(myReducer);

const action = addCounter(1);

myStore.dispatch(action);
// Error: Actions must be plain objects. Use custom middleware for async actions.

このエラー処理はdispatchの内部で行っており、reducerに渡す前にエラーになっている。
そのため例えば、dispatchを拡張してpromiseオブジェクトをハンドリングしてあげれば、非同期処理の結果をreducerに渡すことが出来る。

dispatch の拡張

以下は、dispatchを上書きして、本来のdispatchの処理の前に非同期処理の結果をハンドリングしている。
こうすることで、reducerにはプレーンなオブジェクトが渡されるようになる。

const originalDispatch = myStore.dispatch;
myStore.dispatch = action => {
  action.then(res => {
    console.log(res); // { type: 'ADD_COUNTER', value: 1 }
    originalDispatch(res);
    console.log(myStore.getState()); // 1
  });
};

const action = addCounter(1);

myStore.dispatch(action);

だが既存のdispatchを上書きしてしまうこの方法は、もちろん望ましくない。
Reduxには、dispatchを拡張するための方法が予め用意されている。
それが Middleware である。

Middleware を使うことで、dispatchの機能を拡張してreducerの前後に任意の処理を追加できるようになる。

applyMiddleware

Middleware を使うには、createStoreの第二引数でapplyMiddlewareを呼び出す。

import {createStore, applyMiddleware} from 'redux';
const myStore = createStore(myReducer, applyMiddleware(myMiddleware));

applyMiddlewareの引数に、適用する Middleware を入れる。
後で改めて説明するが、applyMiddlewareを使用すると、dispatchが実行された際に、dispatchではなく指定した Middleware が呼び出されるようになる。

Middleware は複数指定することが出来るが、まずは理解しやすくするために一つだけ使ってみる。

先程の非同期処理の例を解決するには、次のように書けばよい。

import {createStore, applyMiddleware} from 'redux';

const ADD_COUNTER = 'ADD_COUNTER';

const addCounter = value =>
  Promise.resolve(value).then(res => ({type: ADD_COUNTER, value: res}));

const myReducer = (state = 0, action) => {
  const {type, value} = action;
  switch (type) {
    case ADD_COUNTER:
      return state + value;
    default:
      return state;
  }
};

const myMiddleware = store => next => action => {
  action.then(res => {
    console.log(res); // { type: 'ADD_COUNTER', value: 1 }
    next(res);
    console.log(store.getState()); // 1
  });
};

const myStore = createStore(myReducer, applyMiddleware(myMiddleware));

const action = addCounter(1);

myStore.dispatch(action);

非同期処理のハンドリングを、myMiddlewareで行っている。
次は、このmyMiddlewareを例にして、Middleware がどのように動いているのかを見ていく。

Middleware の構造と引数について

Middleware でまず目を引くのは、store => next => action =>という連続したアロー関数。
最初に見た時は戸惑ったが、これは関数を返す関数、という構造が入れ子になっているだけで、複雑なものではない。
例えば先程のmyMiddlewareは、以下のように書くことも出来る。

function myMiddleware(store) {
  return function(next) {
    return function(action) {
      action.then(res => {
        console.log(res); // { type: 'ADD_COUNTER', value: 1 }
        next(res);
        console.log(store.getState()); // 1
      });
    };
  };
}

先述のように、 Middleware はdispatchしたときに呼ばれる。
つまりmyStore.dispatch(action)としたときに、以下が実行される。

myMiddleware(store)(next)(action)

もちろんこれは単純化したもので、実際にはこんなシンプルな実装ではないだろうが。

次に、Middleware に渡される3つの引数について見ていく。

const myMiddleware = store => next => action => {
  console.log(store); // { getState: [Function: getState], dispatch: [Function: dispatch] }
  console.log(next); // [Function: dispatch]
  console.log(action); // Promise { <pending> }
};

storecreateStoreで作ったstoreだが、それそのものではなく、持っているメソッドはgetStatedispatchのみ。
nextは、Middleware を適用する前の、本来のdispatch。Middleware を複数使う時は必ずしもそうはならないのだが、 Middleware が一つのときは必ずdispatchになる。
actionは、myStore.dispatch(action)dispatchに渡されたaction

nextactionが重要で、next(action)とすると、本来のdispatch(action)を呼び出すことになる。
つまり、actionreducerに渡す。

これにより、reducerの前後に任意の処理を追加して、promiseオブジェクトをハンドリングしたり、reducerによって更新されたあとのstateを取得したりすることが出来るのである。

const myMiddleware = store => next => action => {
  action.then(res => { // reducer に渡す前にpromiseオブジェクトを処理
    next(res); // ここで、dispatch して reducer に res を渡している
    console.log(store.getState()); // reducer 実行後の state を取得
  });
};

Middleware のなかでnextを呼ばない、ということも可能ではある。
その場合、dispatchせず、つまりreducerを実行せずに、処理が終わる。

const myMiddleware = store => next => action => {
  console.log('myMiddleware は実行されるが next は呼ばない');
};

const myStore = createStore(myReducer, applyMiddleware(myMiddleware));

myStore.dispatch({type: ADD_COUNTER, value: 1});

console.log(myStore.getState());
// myMiddleware は実行されるが next は呼ばない
// 0

Middleware のチェーン

ここまでは、 Middleware が一つだけのパターンを見てきた。
では、applyMiddleware(middleware1, middleware2)のように複数の Middleware を使うとどうなるのか。
これが Middleware の大きな特徴の一つだと思うが、 Middleware の処理をチェーンのように連続でつないでいくことが可能になる。

これも先に例を示す。
addOnedoubleという2つの Middleware を使っている。
それによるstateの変化も載せておく。

import {createStore, applyMiddleware} from 'redux';

const ADD_COUNTER = 'ADD_COUNTER';

const myReducer = (state = 0, action) => {
  const {type, value} = action;
  switch (type) {
    case ADD_COUNTER:
      return state + value;
    default:
      return state;
  }
};

const addOne = store => next => action => {
  next(action + 1);
};

const double = store => next => action => {
  next({type: ADD_COUNTER, value: action * 2});
};

const myStore = createStore(myReducer, applyMiddleware(addOne, double));

console.log(myStore.getState()); // 0
myStore.dispatch(3);
console.log(myStore.getState()); // 8

applyMiddleware(addOne, double)としてmyStore.dispatchを実行するとまず、addOneが呼ばれる。

actionにはmyStore.dispatchに渡した値が入っており、これは既に説明した内容と同じ。

変わるのはnext。Middleware が一つのときは本来のdispatchが渡されていたが、今回のケースでは、doubleを指す。
そのため以下のようになる。

const addOne = store => next => action => {
  console.log(action); // 3
  next(action + 1); // double を実行
};

そして呼び出されたdouble
actionには、先程のnextに渡した値(action + 1)が入っている。
そしてdoubleにおけるnextは、本来のdispatchを指す。

const double = store => next => action => {
  console.log(action); // 4
  next({type: ADD_COUNTER, value: action * 2}); // dispatch を実行して reducer に引数を渡す
};
const myReducer = (state = 0, action) => {
  const {type, value} = action;
  switch (type) {
    case ADD_COUNTER:
      console.log(action); // { type: 'ADD_COUNTER', value: 8 }
      return state + value;
    default:
      return state;
  }
};

このようにnext()を使うことで、applyMiddlewareに渡した順番で Middleware を実行していくことになる。
その際、next()の引数に値を渡すことで、任意の値をactionとして渡すことが出来る。
そして最後尾の Middleware がnextを実行したときに、dispatchが呼び出され、reducerが実行される。

そのため、最後尾のnext()の引数は、必ずプレーンなオブジェクトでないといけない。
途中の過程ではnext()の引数はどんなものでも構わないが、最後尾のnext()はそのままdispatch()を呼び出すため、プレーンなオブジェクト以外を渡すとエラーになってしまう。

const double = store => next => action => {
  console.log(action); // 4
  next(action * 2); // ここでエラーになる
};

next の複数回使用、store.dispatch の使用

応用と言うほどの内容ではないが、こういう書き方も出来るという例を、復習も兼ねて書いておく。

nextは、一つの Middleware のなかで複数回使うことが出来る。

const twice = store => next => action => {
  next(action);
  next(action);
};

const myStore = createStore(myReducer, applyMiddleware(twice));

console.log(myStore.getState()); // 0
myStore.dispatch({type: ADD_COUNTER, value: 5});
console.log(myStore.getState()); // 10

Middleware に渡されたstoredispatchメソッドを持っているが、これはmyStore.dispatchと同じであり、これを呼び出すと再び先頭の Middleware から実行することになる。

const first = store => next => action => {
  console.log('first', action);
  next(action);
};

const second = store => next => action => {
  console.log('second', action);
  if (action < 3) {
    store.dispatch(action + 1);
    return;
  }
  next({type: ADD_COUNTER, value: action});
};

const myStore = createStore(myReducer, applyMiddleware(first, second));

console.log(myStore.getState());
myStore.dispatch(1);
console.log(myStore.getState());

// 0
// first 1
// second 1
// first 2
// second 2
// first 3
// second 3
// 3

Reactとの連携

最後に、ReactアプリでAPIを叩く例を書いてみる。

ReactとReduxの接続については、冒頭に貼った記事などを参照。

import React from 'react';
import ReactDOM from 'react-dom';
import {createStore, applyMiddleware} from 'redux';
import {Provider, connect} from 'react-redux';

const API_CALL = 'API_CALL';
const REFRESH_STATUS = 'REFRESH_STATUS';

function App({status, myActionCreateor}) {
  return (
    <div>
      <p>通信状態:{status}</p>
      <button
        onClick={() => {
          myActionCreateor(true);
        }}
      >
        通信(成功する)
      </button>
      <button
        onClick={() => {
          myActionCreateor(false);
        }}
      >
        通信(失敗する)
      </button>
    </div>
  );
}

const myReducer = (state = {status: '通信前'}, action) => {
  switch (action.type) {
    case API_CALL:
      return Object.assign({}, state, {
        status: '通信中……',
      });
    case REFRESH_STATUS:
      return Object.assign({}, state, {
        status: action.status,
      });
    default:
      return state;
  }
};

const fetchApi = bool =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (bool) {
        resolve();
      } else {
        reject();
      }
    }, 3000);
  });

const myActionCreateor = bool => ({type: API_CALL, result: bool});

const apiHandler = store => next => action => {
  next(action);
  if (action.type !== API_CALL) return;
  fetchApi(action.result)
    .then(() => {
      next({type: REFRESH_STATUS, status: '通信成功'});
    })
    .catch(() => {
      next({type: REFRESH_STATUS, status: '通信失敗'});
    });
};

const myStore = createStore(myReducer, applyMiddleware(apiHandler));

const mapStateToProps = state => ({status: state.status});
const mapDispatchToProps = {myActionCreateor};

const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App);

ReactDOM.render(
  <Provider store={myStore}>
    <ConnectedApp />
  </Provider>,
  document.querySelector('#app')
);

参考資料