JavaScript で学ぶ
関数型プログラミング
1章
Topotal 輪読会
髙村成道(@nari_ex)
2015/1/22
1.1 JavaScript に関する事実
関数型プログラミング言語をサポート
引数として関数を渡すことができる
[1,2,3].forEach(alert);
// アラート"1"がポップアップ
// アラート"2"がポップアップ
// アラート"3"がポップアップ
→ 引数に渡した関数に配列の要素を順番に渡して実行する
JavaScript の驚異的な柔軟性
JavaScript の三銃士
→ apply, arguments, call
apply メソッド
• すべての関数において実装されているメソッド
• 引数 1 つ目: this オブジェクト
• 引数 2 つ目: 配列
fun.apply(thisArg, array)
• 与えられた配列を関数の引数として渡して実行することがで
きる
apply メソッドを用いた例
function splat(fun) {
return function(array) {
// null を指定すると暗黙的にグローバルオブジェクトに変換される
return fun.apply(null, array);
};
}
var addArrayElements = splat(function(x, y) { return x + y });
addArrayElements([1, 2]);
//! 3
返り値として関数を返す関数
→ 関数型プログラミングの入り口!
arguments 変数
• すべての関数内で arguments ローカル変数にアクセス可能
• 関数呼び出し時に引数として与えられた値を保持
function func() {
for (var i = 0, l = arguments.length; i < l; i++) {
alert(arguments[i]);
}
};
func(1,2,3)
//! 1
//! 2
//! 3
call メソッド
apply と同様に、引数に与えられた値を関数に渡して実行
第2引数に、配列ではなく個別の引数を受け取る
fun.call(thisArg, [, arg1 [, arg2, ...]]);
call メソッドと arguments 変数を用いた例
• 任意の引数を渡すことが可能
function unsplat(fun) {
return function() {
return fun.call(null, _.toArray(arguments));
};
}
var joinElements = unsplat(function(array) { return array.join(' ') });
joinElements(1, 2);
//! "1 2"
joinElements('-', '$', '/', '!', ':');
//! "- $ / ! :"
1.1.1 JavaScript の制限
欠点
• 奇妙な仕様
• 安全性に欠けている
• グローバルスコープに依存している
• 命令型プログラミングをサポート
• 競合するライブラリの山
• 多くのモジュールが実装されたがそれぞれに互換性はない
命令型プログラミングと
宣言型プログラミング
例: 「レジの会計で合計値を計算する」
補足: 命令型プログラミング
1. 品物がカゴに残っていれば 2. へ、空であれば 5. へ進む。
2. 品物を取り出す。
3. 価格を合計値に足す。
4. 1. へ戻る。
5. 合計値を表示する。
→ 順番が異なる場合に破綻する
補足: 宣言型プログラミング
1. 合計値とは、カゴの中の品物をあるルールで処理した結果
• ルールA. 品物が0個の場合、合計値は0である。
• ルールB. 品物が1個以上の場合、合計値は、カゴの中の品
物1つの価格と、残りの品物の合計値を足した値である。
→ 順序関係なし、安全かつ簡潔
欠点はあるがしかし...
• 一定の規則を遵守することで一筋の光を見出すことができる
• 安全性が確保される
• コードベースのサイズに比例してスケーラビリティが増す
• シンプルでわかりやすく、テストしやすいコードになる
→ 本書では、それらを関数型プログラミングによって導く
1.2 関数型プログラミングを始めるた
めに
関数型プログラミングとは
値を抽象の単位に変換する関数を使用して行うプログラミング
であり、それらを使ってソフトウェアシステムを構築すること
1.2.1 なぜ関数型プログラミングが必
要なのか
オブジェクト指向型のシステム
• オブジェクトをたくさん組み合わせることでゴールを目指す
• 例: ボタンを押して情報を出すアプリケーション
• ShowButton → HiddenPanel → InfoPanel...
オブジェクト同士が互いに関連している
→ オブジェクトの変更や追加をしようとしたときに、システム
の状態を考慮する必要がある
関数型のシステム
• 関数を組み合わせて(合成して)値を変換していく
• 例: markdown → toHTML → postProcess → modifyDOM
• postProcessは、visit,post,addId,genId 関数で構成
機能追加時は新しい関数の動作を理解すればよい。
データ変換を繰り返し行うことでシステムを構築するのが関数
型プログラミング。
1.2.2 抽象単位としての関数
抽象化
抽象化の方法として、実装詳細を隠 することが挙げられる
ここでは、エラーや警告を報告するプログラムを考える
function parseAge(age) {
if (!_.isString(age)) throw new Error('引数は文字列である必要があります');
var a;
console.log("age を数値に変換しようとしています");
a = parseInt(age, 10);
if (_.isNaN(a)) {
console.log(["age を数値に変換できませんでした : ", age].join(''));
a = 0;
}
return a;
}
parseAge("42");
// age を数値に変換しようとしています
//! 42
parseAge(42);
// Error: 引数は文字列である必要があります
parseAge("frob");
// age を数値に変換しようとしています
// age を数値に変換できませんでした : frob
//! 0
サンプルコードの問題点
• エラー内容を変えたくなった時にそれぞれを出力する行を変
更する必要がある
• 情報、警告、エラーを使い分けたい場合も同様
関数に抽象化
functon fail(thing) {
throw new Error(thing);
}
function warn(thing) {
console.log(["警告 : ", thing].join(''));
}
function note(thing) {
console.log(["情報 : ", thing].join(''));
}
サンプルコード 改
function parseAge(age) {
if (!_.isString(age)) fail('引数は文字列である必要があります'); ☆
var a;
note("age を数値に変換しようとしています"); ☆
a = parseInt(age, 10);
if (_.isNaN(a)) {
warn(["age を数値に変換できませんでした : ", age].join('')); ☆
a = 0;
}
return a;
}
1.2.3 カプセル化と隠
オブジェクト指向言語の場合
データの要素のパッケージングにオブジェクトの境界を使用
メソッド経由でデータを操作することでデータをパッケージ化
関数型プログラミングの場合
クロージャを使ってデータをカプセル化して隠 する
1.2.4 動作単位としての関数
コンパレータ(comparator)
• 2つの値を引数に取る
• 1つ目の引数が2つ目よりも小さい場合: 負の値を返す
• 1つ目の引数が2つ目よりも大きい場合: 正の値を返す
• 1つ目の引数が2つ目よりも等しい場合: 0を返す
例: sort()
辞書順なので数字の大小に関係なくソートされる
[2, 3, -6, 0, -108, 42].sort();
//! [-108, -6, 0, 2, 3, 42]
[0, -1, -2].sort();
//! [-1, -2, 0]
[2, -3, -1, -6, 0, -108, 42, 10].sort();
//! [-1, -108, -6, 0, 10, 2, 3, 42]
comparator 関数を用いた場合
function compareLessThanOrEqual(x,y) {
if (x < y) return -1;
if (x > y) return 1;
return 0;
}
[2, -3, -1, -6, 0, -108, 42, 10].sort(compareLessThanOrEqual);
//! [-108, -6, -3, -2, -1, 0, 10, 42]
if (compareLessThanOrEqual(1,1)) cosolelog("同じか小さい");
// なにも表示されない
プレディケート(predicate)
常に真偽値を返す関数
function lessOrEqual(x, y) {
return x<=y;
}
if (lessOrEqual(1,1)) consolelog("同じか小さい");
// 同じか小さい
[2, -3, -1, -6, 0, -108, 42, 10].sort(lessOrEqual);
//! [42, 10, 0, -1, -2, -3, -6, -108] ← 逆順になってる
高階関数によるマッピング
関数を引数にとり、新しい関数を生成して返す関数
function comparator(pred) {
return function(x, y) {
if (truthy(pred(x, y)))
return -1;
else if (truthy(pred(y, x)))
return 1;
else
return 0;
};
[100,1,0,10,-1,-2,-1].sort(comparator(lessOrEqual));
//! [-2,-1,-1,0,1,10,100]
1.2.5 抽象としてのデータ
• 関数は、ある世界からある世界への橋渡しを行う
• 例: 文字列→数値, 文字列→配列, 配列→配列の組...
データを抽象的にとらえ、異なるデータへ変換することに意識
を集中させる
1.2.6 関数型テイストの JavaScript
有用な関数その1: existy()
• 存在しないことを判定する
function existy(x) { return x != null };
existy(null);
existy(undefined);
existy({}.notHere);
existy((function(){}());
//! false
existy(0)
existy(false)
//! true
有用な関数その2: truthy()
与えられた値が true とみなされるかどうかを判定する
function truthy(x) { return (x !== false) && existy(x) };
truthy(false);
truthy(undefined);
//! false
truthy(0);
truthy('');
//! true
existy, truthy の応用
true なら実行、それ以外なら undefined を返す
function doWhen(cond, action) {
if (truthy(cond))
return action();
else
return undefined;
}
Array#map
配列のそれぞれの要素に対して引数に与えた関数を実行し、
それぞれの実行結果を格納した配列を返す
[null, undefined, 1, 2, false].map(existy);
//! [false, false, ture, true, true]
[null, undefined, 1, 2, false].map(truthy);
//! [false, false, ture, true, false]
つづき
先ほどのコードでは、以下のようなことが行われている
• 関数を装った存在の抽象の定義 (existy)
• 既存の関数を使って構築された真値の抽象の定義 (truthy)
• これらの関数を他の関数のパラメータに渡すことによる新た
な動作の実現 (truthy, existy と map の組み合わせ)
これこそが関数型プログラミング!!!
1.2.7 実行速度について
関数型プログラミングで記述しても遅くならない
よ
• 流行りの V8 エンジンはランタイム最適化してくれるぜ
• オプティマイザは賢いから気張ってコーディングしないでok
• 関数型プログラミングすると必ず速度低下することはない
• 関数型プログラミングはコーディングの時間は短縮する
1.3 Underscore について
• なんで使ったの
• いちいち map とか実装してる暇はない
• map の実装ではなく概念が説明したいんじゃ!
• Underscore は基本的なライブラリがきちんと ってる
• 車輪の再発明しても益少なかろうよ
→ Topotal 1系...
1.4 まとめ
• 入門的なトピックを扱った
• JavaScript アプリケーションを構築する一つの方法が「関数
型プログラミング」
• 抽象を導き出して関数として構築する
• すでに存在する関数を使って、より複雑な抽象を構築する
• すでに存在する関数を別の関数に渡すことによって、さら
に複雑な抽象を構築する

【Topotal輪読会】JavaScript で学ぶ関数型プログラミング 1 章