2013 Minori Yamashita [email protected]
-- ここにあなたの名前を追記 --
- 導入
- JavaScriptは関数型か
- 関数
- The Bad Parts
- =を疑え
- for, while, eachを疑え
- ブロックを疑え
- thisを疑え
- まとめとコーディング規約
- 高階関数
- Underscore.js
- Underscore-fix
- おわり
こんにちは、僕は元気です。この記事では、"リーダブル"で"モジュラー"で"メインテイナブル"なコードを書くために、jsにおける関数型プログラミングについて、ボトムアップに学習していきます。
本題に入る前に、関数型という言葉を整理しておきましょう。この言葉は色々な人が色々な意味で使っています。一端にはArray#eachやNumber#timesなどでクロージャを使えば関数型と呼び、もう一端ではファンクターだアプリカティブだモナドだ圏論だと言った型理論を中心としたプログラムを関数型と呼ぶ、といったようにコンセンサスが取れていません。この記事では、以前書いた記事、LLerのための関数指向入門での 関数指向 の定義をそのまま当てはめ、「関数と値を使ったプログラミングスタイル」を指して関数型プログラミングと呼称することにします。
一連の記事は、関数型プログラミングに親しみのない方に関数型を紹介し、世界に綺麗なコードを広めることを目的としています。人に関数型を紹介する際の参考リンクの一つとして活用して頂けると光栄です。記事はgistで公開しているので、ご自由にforkして頂いて構いません。
それでは、はじまりはじまりー
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を書いていきましょう。
Jsで関数型プログラミングをする上で避けるべき言語機能や、イディオムを見ていきます。
頭にvar
(やlet
)がない行に=
があったら参照透明破壊警報ギャンギャンです。Birthing Processと呼ばれる、無から値を生成する関数の中ではこれが必要になる事もありますが、それ以外で使用していたら、抽象化が足りていないと見て間違いないでしょう。特に=
の左辺が関数への引数として渡ってきた値だったら完全にアウトです。
function foo (x) {
var y = {}; //これはOK
y.bar = 5; //これはギリセーフ
x.baz = y; //これはアウト!
return x;
}
while
、for
、[].forEach
、これらは副作用(再代入、データの破壊など)の存在を示唆します。UI操作や印字など、副作用が本質である操作以外にこれが使われていたら考え直した方が良いでしょう。map
やfilter
などの高レベルな関数の実装以外にはあまり使い道はないと考えましょう。
/* 悪い例 */
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を伴わない(末尾位置にない)if
やswitch
がある場合、その関数は責務が多すぎる可能性が高いです。関数を小分けにすることを検討してください。
例は悪いですが…
/* 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 () { ... }
Jsというと、クラスがないためにthis
が動的に決定されるという特徴のために混乱を来たし、方々でトンチンカンな記事が書かれる原因となっています。慣れれば別に難しくはないのですが、コード中にthis
が出てくる度に呼び出し元の心配をするのはバカらしいです。関数型jsでは、データ構造のハッシュマップとして以外にオブジェクトを使わないので、そもそもthis
が全く必要ありません。
色々とクリティサイズしましたが、実はこれを全て覚える必要はありません。2つだけ守れば他も勝手に付いてくるようになっています。というわけで以降のjsコーディング規約はこちらです。
var
の付いていない=
禁止- 関数は小分けに
いきなりfor
やwhile
を否定されて、戸惑った方もいらっしゃるかもしれません。ご安心ください。別にループ禁止というわけでも、全て再帰関数でやれ(これはむしろバッドアイディアです)というわけでもありません。関数型プログラムでは、for
、while
や再帰などの低レベルな構造は、その上に抽象を作って覆い隠し、可能な限り短く、読みやすく、可搬で、宣言的なコードを書きます。
最近のjs処理系のArrayには、そのようなメソッドが既にいくつか備わっています。他のクラスに関してはまだまだですが、とりあえず見てみましょう。map
, filter
, reduce
, every
, some
などです。
//自乗関数
function square (x) { return x * x; }
[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 */
["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が使いこなせれば関数型に慣れてきた一つの目安になると思います。
さて、先ほど、「最近のjs処理系...」と書きましたが、これをあてにしているとIE8以前対応の時などにしっぺ返しを食らいます。処理系間の差異を吸収し、標準で用意されていない便利な関数も様々なクラスに用意したものがUnderscore.jsです。
Underscoreではオブジェクト指向風の書き方と、関数型の書き方が両方できるようになっていますが、ラッピングはコストも高いし、もともと関数型プログラミング支援のライブラリなので、個人的には後者を使うことが多いです。
書き方が変わるとはいっても、先ほどと大してかわりません。
_.map([1,2,3,4,5], function (x) { return x * x; });
Underscoreのページで、Collections用とされている関数は、ObjectとArrayに総称化されています。つまり、同じ関数をハッシュマップと配列、両方に適用できます。ここで引数の型に関わらず配列が帰ってくるのが歯がゆいところですが、便利なこともままあります。
_.map({ a: 1, b: 2, c: 3 }, function (x) { return x * x; }); //=> [1, 4, 9]
Underscoreで個人的によく使うのはCollections, Objectsの各関数と、Functionsのpartial
とcompose
、Arraysのrange
とuniq
、Utilitiesのidentity
とtemplate
あたりです。使い始めるのに全て覚える必要はなく、少しづつ発見していくと楽しいです。Underscoreについて詳しくはUnderscoreのページを見てください。
関数型プログラミングに不慣れな方には、これ以降のセクションは荷が重いかもしれません。後日もういちど読みに来てください。本物の関数型プログラミングをお見せしますよ。
関数型言語の経験がある人がUnderscoreを使っていると、痒いところに手が届かないことがわりかしあります。例えばさっきの、map
にオブジェクトを渡しても配列が帰ってきてしまうことや、Underscore規定の引数の順番のせいで部分適用がし辛かったり、jsの演算子が第一級でないせいで簡単な関数を作るのにもいちいちまどろっこしい関数リテラルを書く必要があることなどです。
Underscore-fixは、これらの不都合を解消することを目標としたUnderscoreの拡張ライブラリです。他のライブラリを作るためのヘルパとして作ったので網羅的とはいえませんが、一番便利なのはmerge
、flippar
とoptarg
、各種演算子の関数版あたりでしょうか。とにかく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));
mapmap
あたりは付け焼刃感がぷんぷんするので、今後はmap
やfilter
などに直接fixを施そうと考えています。each
が配列とオブジェクトのイテレートに共通のインタフェースを作ったように、合成にも共通のインタフェース(Clojureのconjみたいなの)を作ればうまいこといくはずです。
毎度締めが適当ですが今回はここまでにします。読んで頂きありがとうございました。これからも関数型エヴァンジェリズムにご協力ください。