Wintus
5/12/2013 - 4:38 PM

Functional JavaScript

Functional JavaScript

Functional JavaScript

2013 Minori Yamashita ympby@gmail.com

-- ここにあなたの名前を追記 --

目次

  • 導入
  • JavaScriptは関数型か
  • 関数
  • The Bad Parts
    • =を疑え
    • for, while, eachを疑え
    • ブロックを疑え
    • thisを疑え
    • まとめとコーディング規約
  • 高階関数
  • Underscore.js
  • Underscore-fix
  • 関数型プログラムの構成
  • おわり

導入

こんにちは、僕は元気です。この記事では、"リーダブル"で"モジュラー"で"メインテイナブル"なコードを書くために、jsにおける関数型プログラミングについて学習していきます。

本題に入る前に、関数型という言葉を整理しておきましょう。この言葉は色々な人が色々な意味で使っています。一端にはArray#eachやNumber#timesなどでクロージャを使えば関数型と呼び、もう一端ではファンクターだアプリカティブだモナドだ圏論だと言った型理論を中心としたプログラムを関数型と呼ぶ、といったようにコンセンサスが取れていません。この記事では、以前書いた記事、LLerのための関数指向入門での 関数指向 の定義をそのまま当てはめ、「関数と値を使ったプログラミングスタイル」を指して関数型プログラミングと呼称することにします。

一連の記事は、関数型プログラミングに親しみのない方に関数型を紹介し、世界に綺麗なコードを広めることを目的としています。人に関数型を紹介する際の参考リンクの一つとして活用して頂けると光栄です。記事はgistで公開しているので、ご自由にforkして頂いて構いません。

それでは、はじまりはじまりー

JavaScriptは関数型か

jsは関数型プログラミングに向いているかを考えてみましょう。まずは言語自体を見てみます。

  • ○ 最低要件の第一級関数と関数リテラルは言語に備わっています。
  • × 式ではない制御構造があります。
  • × 型チェッカはありません。
  • △ オブジェクト指向をサポートしています。
  • × 末尾呼び出しの最適化はしません。

いくつかペケが着いていますが、そもそも言語自体の成り立ちを調べると、ブラウザで動くSchemeを作りたかった人がJavaとSchemeとSelfをミックスアンドマッチして作ったものと聞くので、Schemeの部分を使えば素朴な関数型プログラムを書く事は比較的容易です。末尾呼び出しにスタックを消費するのも、forなどの低レベルな機能を使って作った抽象を通せばそんなに問題になることはありません。式でない制御構造については、後のセクション、The Bad Partsで再訪します。

関数

入門編なので、一応関数について触れておきましょう。C、Perl、PHPや旧世代jsから来た方は関数という言葉に馴染みがあるかもしれませんが、多分考えているものとは違います。

関数型における関数とは、「0個以上の値を1つの新しい値にマッピングする(対応させる)値」です。それ以外のことをしていたら、それは関数ではありません。関数に見えて関数でない実例をご紹介します。

/* その1 実引数の破壊 */
function push1 (arr) {
  arr.push(1);
  return arr;
}
var x = [1, 2, 3];
var y = push1(x);
console.log(y); //=> [1, 2, 3, 1]
console.log(x); //=> [1, 2, 3, 1] //xまで変わってしまっている

/* その2 thisの破壊 */
var obj = {
  x: "hello",
  bye: function () {
    this.x = "good bye";
  }
};
console.log(obj.x); //=> "hello"    //ここと
obj.bye();
console.log(obj.x); //=> "good bye" //ここで同じ式を書いたのに違う結果が帰ってくる

/* その3 引数を介さない外変数の参照 */
var x = 5;
function foo () {
  return x + 1;
}

/* その4 UI操作 */
function red (jq) {
  jq.css({backgroundColor: "red"}); //副作用を本質とした操作
  return; //なにも返却していない
}

このうち、4についてはjsではこれを禁止するとできることが限りなく限定されるためある程度は許容することとします。

1、2、3について、それぞれを関数に直したものはこちらになります。

/* その1 */
function push1 (arr) {
  var x = arr.slice(); //Array#sliceは新しい配列を作ってくれる関数型メソッド
  x.push(1); //これは破壊的メソッドだが、xはこの関数内で作られた配列なので問題なし
  return x;
}
var x = [1, 2, 3];
var y = push1(x);
console.log(y); //=> [1, 2, 3, 1]
console.log(x); //=> [1, 2, 3]    //xはそのまま

/* その2 */
function bye (o) {
  return _.merge(o, {x: "good bye"}); //_.mergeについては後々説明します
}
var obj = { x: "hello" };
console.log(obj.x);         //=> "hello"
console.log(bye(obj).x);    //=> "good bye"
console.log(obj.x);         //=> "hello"

/* その3 */
function foo (x) {
  return x + 1;
}
foo(5);

関数はステートレスであり、与えられた引数のみに依存します。同じ関数を同じ値に適用して、違う結果が帰ってきたらそれはステートフルななにか、メソッドとかサブルーチンとか呼ばれるものであり、古き悪しき goto とあまり変わりません。

以降のセクションでは、この関数を使ってjsを書いていきましょう。

The Bad Parts

この記事のメインコンテンツです。Jsで関数型プログラミングをする上で避けるべき言語機能や、イディオムを見ていきます。

=を疑え

頭にvar(やlet)がない行に=があったら参照透明破壊警報ギャンギャンです。Birthing Processと呼ばれる、無から値を生成する関数の中ではこれが必要になる事もありますが、それ以外で使用していたら、抽象化が足りていないと見て間違いないでしょう。特に=の左辺が関数への引数として渡ってきた値だったら完全にアウトです。

function foo (x) {
  var y = {}; //これはOK
  y.bar = 5;  //これはギリセーフ
  x.baz = y;  //これはアウト!
  return x;
}

while, for, Array#forEachを疑え

whilefor[].forEach、これらは副作用(再代入、データの破壊など)の存在を示唆します。UI操作や印字など、副作用が本質である操作以外にこれが使われていたら考え直した方が良いでしょう。mapfilterなどの高レベルな関数の実装以外にはあまり使い道はないと考えましょう。

/* 悪い例 */
function doubleAllBad (arr) {
  var i = 0,
      l = arr.length;
  for (; i < l; ++i)
    arr[i] = arr[i] * 2;
  return arr;
}
doubleAllBad([1, 2, 3, 4, 5]);

/* 良い例 */
function doubleAllGood (arr) {
  return arr.map(function (x) {
    return x * 2;
  });
}
doubleAllGood([1, 2, 3, 4, 5]);

ブロックを疑え

functionとオブジェクトリテラル以外で{}が出てきたら要注意です。これも副作用がないと意味をなさない構文です。なるべくブロックは使用しないようにしましょう。もし正当な理由でブロックが必要な場合があったら、関数に括りだすと良いでしょう。

functionに付いている{}は除外しましたが実はこれにも注意が必要で、例えばreturnを伴わない(末尾位置にない)ifswitchがある場合、その関数は責務が多すぎる可能性が高いです。関数を小分けにすることを検討してください。

例は悪いですが…

/* Bad */
function sort (x) {
  var arr;
  if (typeof x === "string") {
    arr = Array.prototype.slice.call(x); //varが着いていない=は注意!
    arr.sort();
    return arr.join("");
  } else if (x instanceof Array) {
    arr = x.concat([]); //xのコピー
    arr.sort();
    return arr;
  }
}

/* Good  */
function sort (x) {
  if (typeof x === "string")
    return sortString(x);
  if (x instanceof Array)
    return sortArray(x);
}
function sortString () { ... }
function sortArray () { ... }

thisを疑え

Jsというと、クラスがないためにthisが動的に決定されるという特徴のために混乱を来たし、方々でトンチンカンな記事が書かれる原因となっています。慣れれば別に難しくはないのですが、コード中にthisが出てくる度に呼び出し元の心配をするのはバカらしいです。関数型jsでは、データ構造のハッシュマップとして以外にオブジェクトを使わないので、そもそもthisが全く必要ありません。

まとめとコーディング規約

色々とクリティサイズしましたが、実はこれを全て覚える必要はありません。2つだけ守れば他も勝手に付いてくるようになっています。というわけで以降のjsコーディング規約はこちらです。

  • varの付いていない=禁止
  • 関数は小分けに

高階関数

いきなりforwhileを否定されて、戸惑った方もいらっしゃるかもしれません。ご安心ください。別にループ禁止というわけでも、全て再帰関数でやれ(これはむしろバッドアイディアです)というわけでもありません。関数型プログラムでは、forwhileや再帰などの低レベルな構造は、その上に抽象を作って覆い隠し、可能な限り短く、読みやすく、可搬で、宣言的なコードを書きます。

最近のjs処理系のArrayには、そのようなメソッドが既にいくつか備わっています。他のクラスに関してはまだまだですが、とりあえず見てみましょう。map, filter, reduce, every, someなどです。

//自乗関数
function square (x) { return x * x; }

/* map */
/* 配列のそれぞれの要素について関数を呼び出し、その返り値で構成された配列を作る */
[1, 2, 3, 4, 5].map(square);     //=> [1, 4, 9, 16, 25]

同じことを手続き的に表現しようとすると、以下のようになるでしょう。

var arr = [1, 2, 3, 4, 5];
var i = 0, l = arr.length;
var arr2 = [];
for (; i < l; ++i)
  arr2.push(arr[i] * arr[i]);

これでは読み辛いですよね。

他のメソッドについても一気に見ていきましょう。

/* filter */
/* 配列のそれぞれの要素について関数を呼び出し、真を返したものだけで構成された配列を作る */
[1,5,2,6,3,7].filter(function (x) { return x < 4; }); //=> [1,2,3]

/* reduce */
/* 別名fold,畳み込み関数. 配列の中身を順に関数に通していき、一つの値に収縮する */
["he", "ll", "o"].reduce(function (acc, x) { return acc + x; }); //=> "hello"

/* every */
/* 配列のそれぞれの要素について関数を呼び出し、全て真を返せば真 */
[1,2,3,4,5].every(function (x) { return x < 6 }); //=> true
[1,2,3,4,5].every(function (x) { return x > 3 }); //=> false

/* some */
/* 配列のそれぞれの要素について関数を呼び出し、一つでも真を返せば真  */
[1,2,3,4,5].some(function (x) { return x < 2; }); //=> true
[1,2,3,4,5].some(function (x) { return x > 6; }); //=> false

これらの関数に共通するのは、引数として関数を受け取り、その関数を使って新しい値を作っているという特徴です。このように関数を受け取ったり、(今回は見ませんが)関数を返却したりする関数のことを 高階関数 と呼びます。このうちreduceが一番強力で、mapやfilterやsumなど、他の関数もこれをもとに定義できます。reduceが使いこなせれば関数型に慣れてきた一つの目安になると思います。

Underscore.js

さて、先ほど、「最近のjs処理系...」と書きましたが、これをあてにしているとIE8以前対応の時などにしっぺ返しを食らいます。処理系間の差異を吸収し、標準で用意されていない便利な関数も様々なクラスに用意したものがUnderscore.jsです。

Underscoreではオブジェクト指向風の書き方と、関数型の書き方が両方できるようになっていますが、もともと関数型プログラミング支援のライブラリなので、個人的には後者を使うことが多いです。

書き方が変わるとはいっても、先ほどと大してかわりません。

_.map([1,2,3,4,5], function (x) { return x * x; });

Underscoreのページで、Collections用とされている関数は、ObjectやArrayやString等に総称化されています。つまり、同じ関数をハッシュマップと配列、両方に適用できます。ここで引数の型に関わらず配列が帰ってくるのが歯がゆいところですが、便利なこともままあります。

_.map({ a: 1, b: 2, c: 3 }, function (x) { return x * x; }); //=> [1, 4, 9]

Underscoreで個人的によく使うのはCollections, Objectsの各関数と、Functionsのpartialcompose、Arraysのrangeuniq、Utilitiesのidentitytemplateあたりです。使い始めるのに全て覚える必要はなく、少しづつ発見していくと楽しいです。Underscoreについて詳しくはUnderscoreのページを見てください。

関数型プログラミングに不慣れな方には、これ以降のセクションは荷が重いかもしれません。部分適用、関数合成などに慣れてきたらもういちど読みに来てください。本物の関数型プログラミングをお見せしますよ。

Underscore-fix

関数型言語の経験がある人がUnderscoreを使っていると、痒いところに手が届かないことがちょくちょくあります。例えばさっきの、mapにオブジェクトを渡しても配列が帰ってきてしまうことや、Underscore規定の引数の順番のせいで部分適用がし辛かったり、jsの演算子が第一級でないせいで簡単な関数を作るのにもいちいちまどろっこしい関数リテラルを書く必要があることなどです。

Underscore-fixは、これらの不都合を解消することを目標としたUnderscoreの拡張ライブラリです。他のライブラリを作るためのヘルパとして作ったのでまだ網羅的とはいえませんが、一番便利なのはmergeflipparoptarg、各種演算子の関数版あたりでしょうか。とにかくfunctionとタイプする回数が激減します。

/* Merge */

  var o = {a:1, b:2};

  /* fixなし */
  var x = _.clone(o);
  _.extend(x, {b: 3, c: 4}); //=> {a:1, b:3, c:4}

  /* fixあり */
  _.merge(o, {b: 3, c: 4});  //=> {a:1, b:3, c:4}

/* Flippar  */

  /* fixなし */
  var join_with_sharp1 = function (arr) {
    return arr.join("#");
  };
  join_with_sharp1(["aaa", "bbb", "ccc"]); //=> "aaa#bbb#ccc"


  /* fixあり */
  var join_with_sharp2 = _.flippar(_.join, "#");
  join_with_sharp2(["aaa", "bbb", "ccc"]); //=> "aaa#bbb#ccc"

/* Optarg */

  /* fixなし */
  var f1 = function (a, b /* , & rest */) {
    var rest = Array.prototype.slice.call(arguments, 2);
    //...
  }

  /* fixあり */
  var f2 = _.optarg(2, function (a, b, rest) {
    //...
  });

/* 演算子 */

  /* fixなし */
  _.map([1,2,3,4,5], function (x) { return x * 2; });
  _.filter([1,2,0,4,1,3,1], function (x) { return x > 2 });

  /* fixあり*/
  _.map([1,2,3,4,5], _.partial(_["*"], 2));
  _.filter([1,2,0,4,1,3,1], _.flippar(_.gt, 2));

/* Mapmap */

  /* fixなし */
  _.reduce({a:1, b:2, c:3}, function (acc, v, k) {
    acc[k] = v + 2;
    return acc;
  }, {});

  /* fixあり*/
  _.mapmap({a:1, b:2, c:3}, _.partial(_["+"], 2));

関数型プログラムの構成

関数型プログラムを書くには、mapreduceなどの関数を使うだけでは不十分です。このセクションでは、プログラム全体の構成を見ていきます。

ここで説明するのはjsに向いていると筆者が考える、Lispを源流とする考え方です。MLやHaskellなどの型原理主義な考え方とはいくつかの点で相違があります。

クラスはない。値は値として扱う。

関数型プログラムには、シングルディスパッチのオブジェクト指向言語(JavaやC++やRuby)にあるクラスは出てきません。オブジェクト指向では、責務の少ない(= 少数のメソッドを持った)たくさんのクラス(型)でプログラムを構成しますが、関数型プログラムではそれとは真逆に、少数の型に対してたくさんの関数を定義していく形を取ります。

It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures. "Epigrams in Programming", by Alan J. Perlis

Jsでは昔からundefined, null, 真偽値, 数値, 文字列, 配列, ハッシュマップ(辞書)がよく使われてきました。オブジェクト指向jsではこれらのクラスを継承して、いくつも新しい型を作ることになりますが、関数型jsでは普通はこれ以上型は必要ありません。これも良い例が思いつかなかったのですが、例えばSetを実装するとしましょう。JsでSetを作る時は内部表現として値をkey、真偽値をvalueとした辞書を使うのがイディオマティックに浸透しています。これをオブジェクト指向と関数型両方で表現してみましょう。

/* オブジェクト指向 */
function Set () {
  this._set = {};
}
Set.prototype.has = function (x) {
  return !! this._set[x];
};
Set.prototype.put = function (x) {
  this._set[x] = true;
};
Set.prototype.rem = function (x) {
  return this._set[x] = false;
};

/* 関数型 */
function make_set () {
  return {};
}
function set_has (s, x) {
  return !! _.at(s, x);
}
function set_put (s, x) {
  return _.assoc(s, x, true);
}
function set_rem (s, x) {
  return _.assoc(s, x, false);
}

関数の方は_.at_.assocが扱える型の値であればなんでもSetとして扱えるというところがミソです。Jsは軽量言語なので軽量言語らしく型を気にせずコードを書きたいものです。

汎用 vs. 専用

筆者の浅い経験上、良い関数型アプリケーションプログラムは、抽象化を繰り返す結果として約7割が汎用な関数定義で構成されています。汎用な関数というのは、そのアプリケーション以外でも便利に使える関数という意味です。つまり、関数型プログラムを一つ作ると、機能満載のライブラリが一つ以上できてしまいます。

少し汎化の練習をしてみましょう。あなたはjQueryを使ってウェブサービスを作っているとします。ウェブサービスにはいくつかフォームがあり、それぞれのフォームにおいて入力が必須の項目が全て埋められるまで送信ボタンを無効化しておきたいとしましょう。

<form>
  <input type="text"   name="name"  id="form1-name" />
  <input type="text"   name="age"   id="form1-age" />
  <input type="submit" value="送信" id="form1-submit" disabled />
</form>
<form>
  <input type="text"   name="color" id="form2-color" />
  <input type="text"   name="sound" id="form2-sound" />
  <input type="submit" value="送信" id="form2-submit" disabled />
</form>

最初のバージョンはこんな感じになるでしょう。

function form1_check () {
  return $("form1-name").val() !== ""
      && $("form1-age").val()  !== "";
}

$("#form1-name,#form1-age").on("change", function () {
  if (form1_check())
    $("#form1-submit").attr("disabled", false);
  else $("#form1-submit").attr("disabled", true);
});

function form2_check () {
  return $("form2-color").val() !== ""
      && $("form2-sound").val() !== "";
}

$("#form2-color,#form2-sound").on("change", function () {
  if (form1_check())
    $("#form2-submit").attr("disabled", false);
  else $("#form1-submit").attr("disabled", true);
});

フィールドの名前がハードコードしてあったり、form1とfom2用にそっくりなコードが2つずつあったりと、見ていて寒気のするコードですが、これくらいのことは現場ではチャメシ・インシデントです。これを汎化するとどうなるでしょうか。

/* 汎用 */

var form_check = function (required_fields) {
  /* 全てのフィールドがvalueを持っているか  */
  return _.every(required_fields, function (fld) {
    return ! _.isEmpty($(fld).val());
  });
};

var enable_submit_when_filled = _.optarg(1, function (submit, fields) {
  $(fields.join(",")).on("change", function () {
      if (form_check(fields))
        $(submit).attr("disabled", false);
      else $(submit).attr("disabled", true);
  });
});

/* 専用 */

enable_submit_when_filled("#form1-submit", "#form1-name",  "#form1-age");
enable_submit_when_filled("#form2-submit", "#form2-color", "#form1-color");

これでアプリケーション専用の行はたった2行になり、ずいぶんと宣言的になりました。定義した関数は汎用なので少しいじればjQueryプラグインにすることだってできます。

$.fn.enableWhenFilled = function (fields) {
  _.apply(_.partial(enable_submit_when_filled, this), fields);
};

$("#form1-submit").enableWhenFilled("#form1-name",  "#form2-age");
$("#form3-submit").enableWhenFilled("#form2-color", "#form2-sound");

純粋関数と副作用の分離

GUIプログラミングでは昔から、プレゼンテーションとドメインの分離が重要とされ、MVCやMVPやMVVMなどといった考え方が生まれ、最近はjsの世界にもこれが浸透してきました。関数型プログラミングでも分離が重要なことがあります。純粋関数と副作用の分離です。

純粋関数は 関数 のセクションで見た関数のことです。add(5, 6)と呼び出すと11が返ってくる時、add5オブジェクトの内部表現をいじって11にしたりしたわけでも、画面に出力したりとかdivの背景を赤くしたりしたわけでもありません。値を渡すと値を返す物、これが純粋関数です。理想的な関数型プログラムは8割以上が純粋関数で構成されます。

副作用とは「5オブジェクトの内部表現をいじって11にしたり」、「画面に出力したり」、「divの背景を赤くしたり」といった、関数の定義からはみ出た行為のことです。副作用を持つ"関数"のことを、これからは サブルーチン と呼ぶ事にします。一つ前のサブセクションで見たenable_submit_when_filledなども関数ではなくサブルーチンです。理想的な関数型プログラミングでは副作用を持つコードは全体の2割以下になるでしょう。

Jsのプログラムで、副作用が必要になるのは主にDOMとのやり取りです。つまりMVCでいうView(Modelの変更を監視してDOMをいじる)とController(DOMのイベントを監視してModelを起動する)です。アプリケーションの心臓であるModel(データとロジック)は基本的に純粋関数のみで構成できます。ただし、MVCそのものはModelが状態を持つ事を前提として設計されているのでそのまま関数型プログラムに適用する事はできません。関数型GUIプログラミングについては筆者もPastaMacaroniなどで現在研究中です。

おわり

毎度締めが適当ですが、今回はここまでにします。読んで頂きありがとうございました。これからも関数型エヴァンジェリズムにご協力ください。