Chapter 08

実例:単体テストとリファクタリング #4 〜カリー化と結合〜

とっくり
とっくり
2022.06.14に更新

数珠つなぎ、書きにくい

純粋関数の最高さを味わったところで、気になっている部分について考えます。ここです。

validator.js
Validator.validateZip = function(state) {
  const state1 = Validator.normalizeToAscii(state);
  const state2 = Validator.normalizeZipFormat(state1);
  const state3 = Validator.checkEmpty(state2, "郵便番号を入力してください");
  return Validator.checkPattern(state3, /^\d{3}-\d{4}$/, "000-0000の形式で入力してください");
};

わざと格好悪く書いてる部分もありますが、state1, state2, state3 ... はいかにも面倒な感じですね。(実際書いてるときに何度も間違えました)

純粋関数でもローカル変数を変更することは問題ないので、再代入可能なletを使って

validator.js
Validator.validateZip = function(state) {
  let result = Validator.normalizeToAscii(state);
  result = Validator.normalizeZipFormat(result);
  result = Validator.checkEmpty(result, "郵便番号を入力してください");
  return Validator.checkPattern(result, /^\d{3}-\d{4}$/, "000-0000の形式で入力してください");
};

でも、もちろん悪くはないです。

変数を一度も使わず

validator.js
Validator.validateZip = function(state) {
  return Validator.checkPattern(
    Validator.checkEmpty(
      Validator.normalizeZipFormat(
        Validator.normalizeToAscii(state)
      ),
      "郵便番号を入力してください"
    ),
    /^\d{3}-\d{4}$/, "000-0000の形式で入力してください"
  );
};

とするのは…いやーこれはないですね。関数を実行する順序が書いた順の逆になってるし読みづらすぎます。

pipe

関数型プログラミングでは「純粋関数同士の組み合わせ方」のパターンがいくつも考案されていて、その決まった名前のパターンを使うことで、知ってる人はその名前を見た瞬間「ああそうやって組み合わせるのか」と理解できるというメリットがあります。

一つの関数の返り値を次の関数に渡す、という今回やりたいことも、pipeという名前がついています。

validator.js
// Validatorではなく別の名前空間にしてみる
const FP = {};
/**
 * @params {Function[]} 複数の関数
 */
FP.pipe = function() {
  const funcs = arguments;
  return function(arg) {
    let result = arg;
    for (let i = 0; i < funcs.length; i++) {
      result = funcs[i](result);
    }
    return result;
  }
};

急にわかりにくいかもですが、引数で与えられた関数を前から数珠つなぎにした関数を新しく作って返す、という関数です。Unixのパイプもこれと同じ意味ですね。(rest parametersはIE11で使えないのでargumentsでやってます)

カリー化

さてpipeを使って結合してみま…

validator.js
Validator.validateZip = FP.pipe(
  Validator.normalizeToAscii,
  Validator.normalizeZipFormat,
  Validator.checkEmpty ...あっ!

ああっ、FP.pipeに与える関数は1引数じゃないといけないので、2引数であるcheckEmptyが渡せません!(わざとらしい)

というわけで、先にcheckEmptyを少し改造します。

validator.js
Validator.checkEmpty = function(messageIfError) {
  return function(state) {
    if (state.value !== "") {
      return {
        ...state,
        valid: true,
        message: ""
      }
    } else {
      return {
        ...state,
        valid: false,
        message: messageIfError
      }
    }
  }
};

引数はエラーメッセージで、返り値は1引数の関数です。こういうテクニックをカリー化といいます。

カリー化された関数の使い方

ここでずっと走らせっぱなしだった単体テストが全部エラーになるので、一番単純なvalidateNameを直します。

validator.js
Validator.validateName = function(state) {
  return Vadliator.checkEmpty("名前を入力してください")(state);
}

引数リストを2つ渡すスタイルにすればテストは通ります。でももっとカッコよくできます。

validator.js
Validator.validateName = Validator.checkEmpty("名前を入力してください");

関数型っぽくなってきました。こうなると、checkEmptyがカリー化された関数だと知らなければなんだかわからないですが、JSDocとかにちゃんと書けばいいんじゃないでしょうか(投げやり)。

ほかも同様に直せばテストが再び通るようになります。validateZipとかは引数リストを2つ渡すスタイルでとりあえず通しましょう。

いよいよpipeで結合

checkPatternは3つの引数があるので、2つだけ先に渡して、Stateを後から受け取る1引数関数を返すようにして…これはカリー化と言わないらしいですが…まあ名より実を取りましょう。

validator.js
Validator.checkPattern = function(pattern, messageIfError) {
  return function(state) {
    :
    :
  }
};

さてここまでで、本命のvalidateZipはこうなっています。

validator.js
Validator.validateZip = function(state) {
  const state1 = Validator.normalizeToAscii(state);
  const state2 = Validator.normalizeZipFormat(state1);
  const state3 = Validator.checkEmpty("郵便番号を入力してください")(state2);
  return Validator.checkPattern(/^\d{3}-\d{4}$/, "000-0000の形式で入力してください")(state3);
};

きれいにstatestate1state2state3 → 返り値 と流れていってますね。これを先程作ったpipeで結合するとこうなります。

validator.js
Validator.validateZip = FP.pipe(
  Validator.normalizeToAscii,
  Validator.normalizeZipFormat,
  Validator.checkEmpty("郵便番号を入力してください"),
  Validator.checkPattern(/^\d{3}-\d{4}$/, "000-0000の形式で入力してください")
);

かっこいいですね!最初からずっと走っている単体テストもパスしています。安心感が(しつこい)

できあがり

ここまでのコードはこちらです。

https://github.com/tockri/not-scary-fp/tree/master/ex-form-validation/refactor-4

関数型は読みやすい?

このvalidateZipのコードは読みやすいでしょうか。僕は読みやすいと感じます。pipeという単語が関数型プログラミングの文脈でどのような働きをするか知っているからです。

  1. 1つの引数をとって
  2. normalizeToAsciinormalizeZipFormatcheckEmptycheckPatternという関数を順番に適用して
  3. 返り値を返す
    という構成になっているであろうことがぱっと読んだだけでわかります。

あと、pipeの後は関数が並んでいるだけなので、バグの入る余地がほとんどないのも嬉しいです。if 〜 else がいくつもネストしてたら、レビューするのも神経つかいますよね。

しかし関数型プログラミングの文脈を知らない人には謎のコードに思えるかもしれません。なので、できるだけ多くの人に関数型プログラミングの文脈を伝えれば、こういうコードが読みやすいと感じる人が増えて、みんな幸せになれるんじゃないかと思います。