みなさまこんばんは。
前回に引き続き、平松 @co_sche (co-sche)です。
この連載では、先日社内で行われた「HTML5, CSS3を舐め回す会 Vol.2 - JavaScript Day -」で私がお話した「JavaScriptで始める関数型プログラミング」の内容を元に、記事にまとめたいと思います。
想定している読者は、
今回は、実務にありがちな例を挙げてFP的なアプローチ方法を見て、FPのメリットを実感してみましょう。
今回のサンプルコードの出力には、console.logを使うことにしますが、環境に応じてprintなりdocument.writeなりに読み替えてください。
そんな時代ですので、
DBから取ってきたデータをキャッシュし、後で再フィルタリングというケースも多々ありますので、サーバーサイドのプログラマも無関係ではありません。さて、
これに対してありがちな要件として、以下のような物があります。
が、今回はもうちょっと「関数脳」寄りな考え方を使ってみましょう。
述語関数とは、真偽値を返す関数(isAccessoryやisKeyholder)です。
断りもなく「再帰的に」という言葉を出したので、勉強会の際にはこの部分で混乱を招きました。
かと言ってここで説明はしませんが、reduceがどう計算されるかのイメージを以下に記載します。
この原理で、上記で定義した countupやjoinNameByNLが効いていく様を確認してください。
これらの関数(filterやreduce)は自分でも作れますが、非常に汎用性の高い抽象なので、いろんな言語で既に用意されています。
reduceについては、言語によってfoldやinjectと様々な名前で定義されています。
また、結合関数によっては右から結合した時と左から結合した時で結果が異なる(割り算を考えてみてください)ため、foldLeftやfoldRightなどでそれぞれ定義されていることがあります。
先のreduceの説明で、わざわざ「左から結合」と言ったのは、このためです。
集合の全てに関数を適用した結果の集合を返す map、集合の要素数を返す countやsizeやlength、文字列の集合を特定の文字で結合する joinやimplodeも多くの言語で定義済みです。
ここに挙げた3種は全てreduceを使って定義できます。実際、上記のreduceとjoinNameByNLの組み合わせは、joinを具体化したものとも見なせますね。
filterやreduceも含めて、これら関数の実装については今後の連載で紹介できればと思います。
filterやmapなどの関数が配列のメソッドになってぶら下がっているので出現順は変わっていますが、やっていることは変わりません。
また、console.logの中身を評価していく段階では副作用が無いことにも注目して下さい。
filterもmapもjoinも、元の配列や外の環境に何の影響も与えません。
ちなみに、定義済みの関数以外をリテラルで書くなら、
引数として渡す関数が汎用的でなく、且つ短く書けるのであれば、いちいち名前を付けずにこのように書くことも可能です。
また、様々な関数型言語で用意されている汎用的な抽象も幾つか学びました。
今回は、第一回でお約束したメリットの振り返りをしてまとめとしたいと思います。
配列のインデックスをインクリメントして…というのは実はやりたいことと無関係ですよね。
それに対して、FP的な考え方で記述したコードの本体では、
本質以外とみなされていた処理は集合の操作へと抽象化され、filterやreduceなどの関数にまとめられています。
元のコードで、あるキーホルダーと思われるアイテムが列挙対象に入っているかどうかをテストするには、
が、テストのためにロジック内部に手を入れるのは、なんとも気が引けます。
対して、モジュール化したコードでは
また、静的型システムと融合すると、さらにこのメリットは強くなります。(JSでは期待できませんが)
そこから関数型で書いても良いし、ベタ書きに落としても良いです。
例えば、配列を見ていきなりfor文で回して…ではなく、集合に対して何かをするんだという見方が出来るだけで、かなりやりたいことの見通しは良くなるはずです。
また、抽象化・部品化が上手にできることによって、設計や実装のより効果的な分散が可能になるのはオブジェクト指向と通ずる利点です。
純粋関数のシンプルな特徴から、様々な抽象・具象が生み出される様は見ていてワクワクしませんか?
魔法のようなreduceの実装がどうなっているのか、覗いてみたくなりませんか?
関数型プログラミングを使いこなすには、汎用性の高い抽象を見つけるセンスと、それをプログラムに落とすテクニックを磨く必要があります。
…なので、次回以降は以下のようなことを話せたらよいなぁと思っています。
α変換、β簡約 etc…
Yコンビネータ、末尾再帰 etc…
参照透明性との関係 etc...
勉強会でお話しした内容は、またこのブログでまとめたいと思いますので、今回の連載でFPに興味を持っていただけた方は[読者になる]や[RSS]機能でチェックしてみて下さい。
では、またお会いできる日までっ!
--
この連載では偉そうに講釈してますが、実は私も勉強中だったりします。
自分が分かりにくかった点を噛み砕いて可能な限り正確に説明しているつもりですが、万一間違いやトンデモ理論が飛び出した時はAmeba Pocket等ブックマークコメントでビシバシツッコミをいただければ幸いです。
JavaScriptで始める関数型プログラミング 関連記事
JavaScriptで始める関数型プログラミング 1 - 1
JavaScriptで始める関数型プログラミング 1 - 2
前回に引き続き、平松 @co_sche (co-sche)です。
この連載では、先日社内で行われた「HTML5, CSS3を舐め回す会 Vol.2 - JavaScript Day -」で私がお話した「JavaScriptで始める関数型プログラミング」の内容を元に、記事にまとめたいと思います。
想定している読者は、
- JavaScriptの基本的な構文を理解している方
- JavaScriptで関数を定義・使用したことのある方
- LLと総称される言語を扱っているが、関数型プログラミングを意識したことのない方
はじめに
前回は、関数型プログラミング(以下、たまにFP)を学ぶ上で欠かせない、関数の特徴について学びました。今回は、実務にありがちな例を挙げてFP的なアプローチ方法を見て、FPのメリットを実感してみましょう。
今回のサンプルコードの出力には、console.logを使うことにしますが、環境に応じてprintなりdocument.writeなりに読み替えてください。
ありがちな例
XMLが文書とはとても呼べないデータを扱う用途に使われだしたり、JSONが発明され、言語を超えて普及したりと移り変わりはあるけれど、最近はMVCの概念の普及に伴い猫も杓子もWeb APIになっていますね。そんな時代ですので、
- サーバーに商品のリストを問い合わせ、JSONで受け取る
- わりと忙しいサーバーなので、ちょっとしたフィルタリングやソート、集計はクライアントでやって欲しい
DBから取ってきたデータをキャッシュし、後で再フィルタリングというケースも多々ありますので、サーバーサイドのプログラマも無関係ではありません。さて、
[
{
"code": "pink_shirt",
"name": "ピンク色のシャツ",
"description": "「ハネムーン」の文字と共に、ハートをあしらいました。",
"price": 2000,
"category": "tops"
},
{
"code": "green_keyholder",
"name": "エメラルドグリーンのキーホルダー",
"description": "ひらがなの「ぶ」をかたどり、上品な色に仕上げました。",
"price": 1500,
"category": "accessory"
},
.....
]
こんなデータを受け取って、itemsと名前を付けたとしましょう。{
"code": "pink_shirt",
"name": "ピンク色のシャツ",
"description": "「ハネムーン」の文字と共に、ハートをあしらいました。",
"price": 2000,
"category": "tops"
},
{
"code": "green_keyholder",
"name": "エメラルドグリーンのキーホルダー",
"description": "ひらがなの「ぶ」をかたどり、上品な色に仕上げました。",
"price": 1500,
"category": "accessory"
},
.....
]
これに対してありがちな要件として、以下のような物があります。
アクセサリーの個数を表示したい
var accessoriesCount = 0;
var itemsCount = items.length;
for (var i = 0; i < itemsCount; i++) {
var item = items[i];
if (item.category == 'accessory') {
accessoriesCount++;
}
}
console.log(accessoriesCount);
var itemsCount = items.length;
for (var i = 0; i < itemsCount; i++) {
var item = items[i];
if (item.category == 'accessory') {
accessoriesCount++;
}
}
console.log(accessoriesCount);
キーホルダーの名前を一覧表示したい
var itemsCount = items.length;
for (var i = 0; i < itemsCount; i++) {
var item = items[i];
if (item.code.indexOf('keyholder') != -1) {
console.log(item.name + "\n");
}
}
for (var i = 0; i < itemsCount; i++) {
var item = items[i];
if (item.code.indexOf('keyholder') != -1) {
console.log(item.name + "\n");
}
}
関数にまとめたい
上記のようなバリエーションがポコポコと増えていくわけですが、それぞれの処理についてfunction (items, categoryName) {
var c = 0;
var imax = items.length;
for (var i = 0; i < imax; i++) {
var item = items[i];
if (item.category == categoryName) {
c++;
}
}
console.log( c );
}
という風にまとめて、関数の名前どうしようとか、console.logは副作用だからcを返すだけにしようとかという汎用化・抽象化の進め方もあるにはあります。var c = 0;
var imax = items.length;
for (var i = 0; i < imax; i++) {
var item = items[i];
if (item.category == categoryName) {
c++;
}
}
console.log( c );
}
が、今回はもうちょっと「関数脳」寄りな考え方を使ってみましょう。
共通部分はどこ?
- 集合の特定条件に合う要素に対して何かしている
- 集合から特定条件に合う要素のみを抽出した新しい集合を作る (フィルタリング)
- 要素全てに対して何かを行う
違う部分はどこ?
- 抽出条件
- 合致時に行う作業
- 元の集合も、時によって違うかもしれない
共通部分・違う部分を踏まえて
FP的に書くとそれぞれの処理はこうなります。// アクセサリーの個数を表示
print(reduce(filter(items, isAccessory), countup, 0));
// キーホルダーの名前一覧を表示
print(reduce(filter(items, isKeyholder), joinNameByNL, ''));
一見ぽかーんとしがちですが、print(reduce(filter(items, isAccessory), countup, 0));
// キーホルダーの名前一覧を表示
print(reduce(filter(items, isKeyholder), joinNameByNL, ''));
- itemsをisAccessoryかどうかでfilterして、0を初期値としてcountupでreduceしたものをprint
- itemsをisKeyholderかどうかでfilterして、''を初期値としてjoinNameByNLでreduceしたものをprint
下準備は必要です
isAccessory、isKeyholder、countup、joinNameByNLは以下のような関数です。function isAccessory(item) {
return item.category == 'accessory';
}
function isKeyholder(item) {
return item.code.indexOf('keyholder') != -1;
}
function countup(a, b) { return a + 1; }
function joinNameByNL(a, b) { return a + "\n" + b.name; }
return item.category == 'accessory';
}
function isKeyholder(item) {
return item.code.indexOf('keyholder') != -1;
}
function countup(a, b) { return a + 1; }
function joinNameByNL(a, b) { return a + "\n" + b.name; }
ちょっと待て
filterって何だ
- 集合と述語関数を引数に取り
- 集合の要素それぞれに対して述語関数を適用し
- 条件に合致した(trueが返った)要素の集合を返す
述語関数とは、真偽値を返す関数(isAccessoryやisKeyholder)です。
reduceって何だ
- 集合と結合関数と初期値を引数に取り
- 再帰的に集合が空になるまで左から結合する
断りもなく「再帰的に」という言葉を出したので、勉強会の際にはこの部分で混乱を招きました。
かと言ってここで説明はしませんが、reduceがどう計算されるかのイメージを以下に記載します。
reduce([a, b, c, d], f, i)
=> reduce([b, c, d], f, f(i, a))
=> reduce([c, d], f, f(f(i, a), b))
=> reduce([d], f, f(f(f(i, a), b), c))
=> reduce([], f, f(f(f(f(i, a), b), c), d))
=> f(f(f(f(i, a), b), c), d)
もっと具体的に、=> reduce([b, c, d], f, f(i, a))
=> reduce([c, d], f, f(f(i, a), b))
=> reduce([d], f, f(f(f(i, a), b), c))
=> reduce([], f, f(f(f(f(i, a), b), c), d))
=> f(f(f(f(i, a), b), c), d)
var add = function(a, b) { return a + b; };
reduce([1, 2, 3, 4], add, 10);
=> reduce([2, 3, 4], add, add(10, 1));
=> reduce([3, 4], add, add(11, 2)); // 11はadd(10, 1)の評価結果
=> reduce([4], add, add(13, 3));
=> reduce([], add, add(16, 4));
=> 20
という具合です。reduce([1, 2, 3, 4], add, 10);
=> reduce([2, 3, 4], add, add(10, 1));
=> reduce([3, 4], add, add(11, 2)); // 11はadd(10, 1)の評価結果
=> reduce([4], add, add(13, 3));
=> reduce([], add, add(16, 4));
=> 20
この原理で、上記で定義した countupやjoinNameByNLが効いていく様を確認してください。
これらの関数(filterやreduce)は自分でも作れますが、非常に汎用性の高い抽象なので、いろんな言語で既に用意されています。
reduceについては、言語によってfoldやinjectと様々な名前で定義されています。
また、結合関数によっては右から結合した時と左から結合した時で結果が異なる(割り算を考えてみてください)ため、foldLeftやfoldRightなどでそれぞれ定義されていることがあります。
先のreduceの説明で、わざわざ「左から結合」と言ったのは、このためです。
さらに定義済みの関数をいくつか
挙げておきましょう。集合の全てに関数を適用した結果の集合を返す map、集合の要素数を返す countやsizeやlength、文字列の集合を特定の文字で結合する joinやimplodeも多くの言語で定義済みです。
ここに挙げた3種は全てreduceを使って定義できます。実際、上記のreduceとjoinNameByNLの組み合わせは、joinを具体化したものとも見なせますね。
filterやreduceも含めて、これら関数の実装については今後の連載で紹介できればと思います。
話は戻って
先ほどの// アクセサリーの個数を表示
console.log(reduce(filter(items, isAccessory), countup, 0));
// キーホルダーの名前一覧を表示
console.log(reduce(filter(items, isKeyholder), joinByNL, ''));
をJSらしく書くと、それぞれconsole.log(reduce(filter(items, isAccessory), countup, 0));
// キーホルダーの名前一覧を表示
console.log(reduce(filter(items, isKeyholder), joinByNL, ''));
console.log(items.filter(isAccessory).length);
console.log(items.filter(isKeyholder).map(getName).join('\n'));
こうなります。console.log(items.filter(isKeyholder).map(getName).join('\n'));
filterやmapなどの関数が配列のメソッドになってぶら下がっているので出現順は変わっていますが、やっていることは変わりません。
また、console.logの中身を評価していく段階では副作用が無いことにも注目して下さい。
filterもmapもjoinも、元の配列や外の環境に何の影響も与えません。
ちなみに、定義済みの関数以外をリテラルで書くなら、
console.log(item.filter(function(item) {
return item.code.indexOf('keyholder') != 1;
}).map(function(item) {
return item.name;
}).join('\n'));
という風になります。return item.code.indexOf('keyholder') != 1;
}).map(function(item) {
return item.name;
}).join('\n'));
引数として渡す関数が汎用的でなく、且つ短く書けるのであれば、いちいち名前を付けずにこのように書くことも可能です。
何をしてきたか
さて、元のfor文とif文の組み合わせと比べると、随分すっきりしたコードになりましたが、何をしたのかをおさらいしましょう。- 着眼点を集合とその要素に分けた
- 共通部分(抽象)を関数にまとめた (reduce, map, filterなど)
- 違うデータや処理を引数として受け取れるようにした (items, isAccessoryなど)
- 結果、モジュール(部品)化ができた
今回のまとめ
はい、実務でありがちなデータや処理について、FP的なアプローチで改善をはかる様子を見てきました。また、様々な関数型言語で用意されている汎用的な抽象も幾つか学びました。
今回は、第一回でお約束したメリットの振り返りをしてまとめとしたいと思います。
1. コードの見通しがいいよ!
今回のケースでは、元のコードのほとんどが本質以外のことで埋め尽くされていました。配列のインデックスをインクリメントして…というのは実はやりたいことと無関係ですよね。
それに対して、FP的な考え方で記述したコードの本体では、
items.filter(isKeyholder).map(getName).join('\n');
と、やっていることの本質が短く表れ、重複が少なくなっています。本質以外とみなされていた処理は集合の操作へと抽象化され、filterやreduceなどの関数にまとめられています。
2.テストしやすいよ!
モジュール(部品)化したことによって、それ毎にテストが出来ます。元のコードで、あるキーホルダーと思われるアイテムが列挙対象に入っているかどうかをテストするには、
var itemsCount = items.length;
for (var i = 0; i < itemsCount; i++) {
var item = items[i];
if (item.code.indexOf('keyholder') != -1) {
print(item.name + "\n");
// if (item.code == itemMaybeKeyholder.code) {
// debug('0K');
// }
}
}
などのようにコメント行部分を挿入するのがお手軽です。for (var i = 0; i < itemsCount; i++) {
var item = items[i];
if (item.code.indexOf('keyholder') != -1) {
print(item.name + "\n");
// if (item.code == itemMaybeKeyholder.code) {
// debug('0K');
// }
}
}
が、テストのためにロジック内部に手を入れるのは、なんとも気が引けます。
対して、モジュール化したコードでは
debug(isKeyholder(itemMaybeKeyholder) === true);
と、単体でテストが可能です。3.バグを作りにくよ!
上記2つのメリット(コードが見やすくテストしやすい)から、自明です。また、静的型システムと融合すると、さらにこのメリットは強くなります。(JSでは期待できませんが)
4.思考の道筋が増えるよ!
今回のように意識してプログラミングをしていると、次第に抽象や部品が先に見えてくるようになります。そこから関数型で書いても良いし、ベタ書きに落としても良いです。
例えば、配列を見ていきなりfor文で回して…ではなく、集合に対して何かをするんだという見方が出来るだけで、かなりやりたいことの見通しは良くなるはずです。
また、抽象化・部品化が上手にできることによって、設計や実装のより効果的な分散が可能になるのはオブジェクト指向と通ずる利点です。
5.楽しいよ!
…あれ?楽しくなかったですか!?純粋関数のシンプルな特徴から、様々な抽象・具象が生み出される様は見ていてワクワクしませんか?
魔法のようなreduceの実装がどうなっているのか、覗いてみたくなりませんか?
次回以降の勉強会では…
今回の勉強会では、FPそのものについてとそのとっかかり部分について話をしました。関数型プログラミングを使いこなすには、汎用性の高い抽象を見つけるセンスと、それをプログラムに落とすテクニックを磨く必要があります。
…なので、次回以降は以下のようなことを話せたらよいなぁと思っています。
ラムダ計算・クロージャ
すべての基礎α変換、β簡約 etc…
カリー化
高階関数との組み合わせでさらに便利に再帰
繰り返しの基礎となるYコンビネータ、末尾再帰 etc…
関数合成
関数と関数を関数で糊付け遅延評価
無限の概念をプログラミングに持ち込んだり、パフォーマンスを上げたりメモ化
パフォーマンスチューニングに使う参照透明性との関係 etc...
勉強会でお話しした内容は、またこのブログでまとめたいと思いますので、今回の連載でFPに興味を持っていただけた方は[読者になる]や[RSS]機能でチェックしてみて下さい。
では、またお会いできる日までっ!
--
この連載では偉そうに講釈してますが、実は私も勉強中だったりします。
自分が分かりにくかった点を噛み砕いて可能な限り正確に説明しているつもりですが、万一間違いやトンデモ理論が飛び出した時はAmeba Pocket等ブックマークコメントでビシバシツッコミをいただければ幸いです。
JavaScriptで始める関数型プログラミング 関連記事
JavaScriptで始める関数型プログラミング 1 - 1
JavaScriptで始める関数型プログラミング 1 - 2