【topotal輪読会】javascript で学ぶ関数型プログラミング 4 章
TRANSCRIPT
JavaScriptで学ぶ関数型プログラミング第4章"高階関数小式澤!篤!(@lastarrow21)
Topotal'輪読会
2015%02%11
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 1
出典• JavaScriptで学ぶ関数型プログラミング
• Michael/Fogus/著
• 和田/祐一郎/訳
• O'REILLY/®/オライリー・ジャパン/出版
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 2
前置き
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 3
前置き• 自由変数をわかりやすくするために、変数名を全て大文字表記にしている
• 一般的なプログラミングスタイルでは推奨されない
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 4
(復習)
クロージャ3.5.1%節
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 5
クロージャ• プログラム内で環境を共有する仕組み
• スコープの実行完了後でも内部情報を保持し続ける関数function makeAdder(CAPTURED) { // CAPTURED : 自由変数 (確保される変数)
return function(free) { return free + CAPTURED };}var add10 = makeAdder(10); // add10 が"10"を確保add10(32); // => 42
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 6
4章"高階関数
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 7
高階関数• 前提
• 第一級のデータ型である
• 次のうち、少なくともどちらか一方を満たす
• 引数として関数をとる
• 関数を戻り値として返す
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 8
高階関数• 前提
• 第一級のデータ型である
• 次のうち、少なくともどちらか一方を満たす
• 引数として関数をとる"!"まずはここから考える
• 関数を戻り値として返す
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 9
4.1$引数として関数を取る関数
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 10
引数として関数を取る関数• 既に(4章よりも前に)登場している
• _.map、_.reduce、_.filter
• 関数を引数に取ることに関して、もう少し詳しく考える
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 11
4.1.1$関数を渡すことを考えるmax、finder、best
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 12
「関数を渡す」と何ができるのか• 配列などから最大値を返す関数"_.max
console.log(_.max([1, 2, 3, 4, 5]));// => 5
• 第二引数に、関数を渡すことができる"!"高階関数var people = [{ name : "Fred", age : 65 }, { name : "Lucy", age : 36 }];console.log(_.max(people, function(p){ return p.age }));// => {name : "Fred", age : 65}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 13
_.maxの考察• 関数を引数に取れる
• 任意のオブジェクト同士を比較する手段を提供している
• 関数を構築する上で有用
• 常に">"(大なり)を用いて比較している
• この制限により、本当に関数型であるとはいえない
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 14
「良い」値を返す関数finder• 第1,$第2引数は関数
• 比較するデータから比較できるように値を作る関数
• 2つの値を比較して、そこから「良い」値を返す関数
• 第3引数は比較するデータを含むオブジェクトの配列
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 15
「良い」値を返す関数finderの実装function finder(valueFun, bestFun, coll) { return _.reduce(coll, function(best, current) { var bestValue = valueFun(best); var currentValue = valueFun(current);
return (bestValue === bestFun(bestValue, currentValue)) ? best : current; });}
finder(_.identity, Math.max, [1, 2, 3, 4, 5]); // => 5
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 16
「良い」値を返す関数finderの他の用例• プロパティ名ageを比較して最大のものを「良い」とするfinder(plucker('age'), Math.max, people); // => { name : "Fred", age : 65 }
• プロパティ名nameの最初の文字がLであるものを「良い」とする
finder(plucker('name'), function(x, y){ return (x.charAt(0) === "L") ? x : y }, people);// => { name : "Lucy", age : 36 }
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 17
「良い」値を返す関数finderの考察• 比較の仕方を関数で指定できる
• 様々なタイプの「最良の値を探す」関数を生成できる
• ロジックを複製している箇所がある// finder関数内return (bestValue === bestFun(bestValue, currentValue)) ? best : current;// 「最良の値を探す」bestFun関数内return (x.charAt(0) === "L") ? x : y;
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 18
ロジックの複製を回避するために• 最良の値を判定する関数は
• 最初の引数が2つ目の引数よりも良い場合にtrueを返す
• 渡された引数を比較できる形まで紐解く方法を知っている
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 19
よりエレガントな関数bestの実装function best(fun, coll){ return _.reduce(coll, function(x, y){ return fun(x, y) ? x : y; });}
console.log(best(function(x, y){ return x > y; }, [1, 2, 3, 4, 5]));// => 5
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 20
より良い値を探す関数の考察• 2つの関数を引数に取る関数#finder
• ロジックの重複が発生した
• ロジックの重複を削除することに一般化した関数#best
• 引き締まった
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 21
より一般的な関数に作り直すには4.1.2%関数を渡すことを更に考える
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 22
指定した回数だけ値を格納する関数!repeat
function repeat(times, VALUE) { return _.map(_.range(times), function() { return VALUE; });}
repeat(4, "Major");// => ["Major", "Major", "Major", "Major"]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 23
関数!repeat!の考察• とても素直な実装
• 「一般的な繰り返し」とは言えない
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 24
関数!repeat!の考察• とても素直な実装
• 「一般的な繰り返し」とは言えない
• 計算を繰り返したいことがある
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 25
関数!repeat!の考察• とても素直な実装
• 「一般的な繰り返し」とは言えない
• 計算を繰り返したいことがある
「値ではなく関数を使え」
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 26
繰り返し計算した結果を格納する関数!repeatedly
function repeatedly(times, fun) { return _.map(_.range(times, fun);}
repeatedly(3, function() { return Math.floor(Math.random() * 10 + 1);});// => [1, 3, 8]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 27
関数!repeatedly!の考察• 関数を呼ぶことによって任意の値が格納されるようになった
• もちろん固定値も格納できる
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 28
関数!repeatedly!の考察• 関数を呼ぶことによって任意の値が格納されるようになった
• もちろん固定値も格納できるrepeatedly(3, function() { return "Odeley!"; });// => { "Odeley!", "Odeley!", "Odeley!" }
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 29
関数!repeatedly!の考察• 関数を呼ぶことによって任意の値が格納されるようになった
• もちろん固定値も格納できるrepeatedly(3, function() { return "Odeley!"; });// => { "Odeley!", "Odeley!", "Odeley!" }
• 引数に関係なく固定値を返す関数はよくある話(詳しくは5章にて)
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 30
「値ではなく関数を使え」(再掲)
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 31
「値ではなく関数を使え」• 関数"repeatedly"で、格納される値が関数によって任意に生成されるようになった
• ただし、反復回数がいつもわかるとは限らない// for文による例for (let i = 2; i <= 1024; i = i * 2) { console.log(i);} // => 2 4 8 16 32 64 128 256 512 1024 // 10回繰り返されたKOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 32
さらに進化した関数!iterateUntil
• 第一引数"fun
• 格納する値を生成する関数
• 第二引数"check
• 繰り返しが終了すべき時にfalseを返す関数
• 第三引数"init
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 33
さらに進化した関数!iterateUntil
function iterateUntil(fun, check, init) { var ret = []; var result = fun(init); while (check(result)) { ret.push(result); result = fun(result); } return ret;}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 34
iterateUntil!の使用例iterateUntil(function(n) { return n * 2; }, function(n) { return n <= 1024; }, 1);// => [ 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 ]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 35
関数!iterateUntil!の考察• 繰り返し条件が関数"check"の結果依存になった
• 実質無制限
• 終了条件だけ知っている場合も用いれる!!関数!repeatedly!の1段階上の存在となった
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 36
高階関数• 前提
• 第一級のデータ型である
• 次のうち、少なくともどちらか一方を満たす
• 引数として関数をとる
• 関数を戻り値として返す"!"次はこっち
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 37
4.2$他の関数を返す関数
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 38
今までの関数を返す関数• makeAdder
• 自由変数と引数の和を返す関数を返す
• complement
• 引数にとった関数の真偽値判定が逆になる関数を返す
• plucker
• 引数にとった値をキーとして対応する値を返す関数を返すKOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 39
定数を返す関数!always
• 関数"repeatedly"では引数を無視して値を返す関数を使った
• 定数を返す関数":"k"と呼ばれる
• ここでは"always"と呼ぶfunction always(VALUE) { return function() { return VALUE; };}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 40
クロージャ使用上の注意!(1/2)
• 関数は常に唯一の値を生成する
• 自由変数VALUEに束縛された戻り値は常に等しい'[Braithwaite,'
'13]
var f = always(function(){});f() === f(); // => true
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 41
クロージャ使用上の注意!(2/2)
• 新しく生成されたクロージャは、それぞれ異なる値を返すvar f = always(function(){});var g = always(function(){});f() === g(); // => false
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 42
関数!always!の使用例• 無名関数の代わりに使うとコードがすっきりするrepeatedly(3, always("Odelay!"));//=> [ "Odelay!", "Odelay!", "Odelay!" ]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 43
関数!invoker
• 第一引数"NAME
• 第二引数のMETHODを実行する対象
• 第二引数"METHOD
• 実行するメソッド名
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 44
関数!invoker!の実装function invoker (NAME, METHOD) { return function(target /* args ... */) { if (!existy(target)) fail("Must provide a target");
var targetMethod = target[NAME]; var args = _.rest(arguments);
return doWhen((existy(targetMethod) && METHOD === targetMethod), function() { return targetMethod.apply(target, args); }); };}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 45
関数!invoker!の使用例var rev = invoker('reverse', Array.prototype.reverse);
_.map([[1,2,3]], rev);//=> [ [ 3,2,1 ] ]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 46
関数!invoker!の考察• invoker"が返す関数はクロージャ
• reverse"などを保持しておいて、後で使用する"(revの設定データ)
• 関数"always"とは違って、特別な処理を行う関数を返す
• 関数型プログラミングでは、オブジェクトを引数に取ることを好む
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 47
4.2.1%引数を高階関数に確保する• Ques&on.*なぜ関数を返す関数を生成するの?
• Hint.*高階関数に渡す引数は、返される関数の設定項目var add100 = makeAdder(100);add100(38); // => 138
• makeAdder関数に100という値を設定したものをadd100という名前に束縛した
• ここからは、変数を確保した関数を返す関数の話KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 48
4.2.2$大義のために変数を確保する
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 49
例!:!ユニークに文字列を生成する関数
素朴な実装の場合function uniqueString(len) { return Math.random().toString(36).substr(2, len);};
uniqueString(10);//=> liydklvjbk
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 50
ランダム文字列に接頭辞を付けたくなったら?
関数!uniqueString!を修正するfunction uniqueString(prefix) { return [prefix, new Date().getTime()].join('');};
uniqueString("argento");//=> "argento1356107740868"
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 51
また要件が変わった!
• 要件":"指定した文字列に連番をつけたものfunction makeUniqueStringFunction(start) { var COUNTER = start; // suffixの開始番号
return function(prefix) { return [prefix, COUNTER++].join(''); }};
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 52
関数!makeUniqueStringFunction!の実行例// 連番は0からスタートvar uniqueString = makeUniqueStringFunction(0);
uniqueString("dari");//=> "dari0"
uniqueString("dari");//=> "dari1"
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 53
オブジェクトによる!makeUniqueStringFunction!の再実装
var generator = { count: 0, uniqueString: function(prefix) { return [prefix, this.count++].join(''); }};generator.uniqueString("bohr"); // => bohr0generator.uniqueString("bohr"); // => bohr1
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 54
オブジェクト!generator!の危険性• count"プロパティに再代入すると"!
generator.counter = "gotcha";console.log(generator.uniqueString("bohr"));// --> bohrNaN
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 55
オブジェクト!generator!の改良var omgenerator = ( function(init) { var COUNTER = init; return { uniqueString: function(prefix) { return [prefix, COUNTER++].join(''); } }})(0);print(omgenerator.uniqueString("lichking-"));
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 56
オブジェクト!omgenerator!の考察• COUNTER"の隠蔽には成功した
• 非常に美しくない
• ECMAScript.next"では頑張っている
• makeUniqueStringFunction"のようにクロージャを使えば美しい
• 参照透明性がないKOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 57
参照透明性• 関数が返す結果がその引数によってのみ左右されること
• makeUniqueStringFunction"が返す値は自身の実行回数依存
• 何回呼び出したかは知ることは不可能
• 詳しくは7章で
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 58
「存在しない状態」var nums = [1,2,3,null,5];_.reduce(nums, function(total, n) { return total * n });//=> 0
doSomething({ WhoCares: 42, critical: null });
//!!"
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 59
「存在しない状態」に備える関数!fnull
• 「存在しない状態」に対する防御のための関数
• fnull"の引数は関数と1つ以上のデフォルト値
• fnull"が返す関数の引数に"null"や"undefined"が混ざっていたら、デフォルト値がかわりに用いられる
• デフォルト値の代入は、必要なときが来るまで遅延される
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 60
関数!fnull!の実装function fnull(fun /*, default value */) { var defaults = _.rest(arguments); return function(/* args */) { var args = _.map(arguments, function(e, i) { return existy(e) ? e : defaults[i]; }); return fun.apply(null, args); };}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 61
関数!fnull!の使用例(1)!:!数値 null
var nums = [1,2,3,null,5];var safeMult = fnull( function(total, n) { return total * n }, 1, 1);_.reduce(nums, safeMult);//=> 30
• nullはデフォルト値"1"に置換される
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 62
関数!fnull!の使用例(2)!:!設定オブジェクトfunction defaults(d) { return function(o, k) { var val = fnull(_.identity, d[k]); return o && val(o[k]); };}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 63
関数!defaults!の利用function doSomething(config) { var lookup = defaults({critical: 108}); return lookup(config, 'critical');}
doSomething({critical: 9});//=> 9
doSomething({});//=> 108
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 64
関数!fnull!の考察• 危険な値(undefinedやnullなど)を気の利いたデフォルト値に変更できる
• o[k] || someDefault$とか書かなくても良くなる
• doSomething$関数と離れた場所で引数のチェックを行える
• 引数の検証ロジックをカプセル化できた
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 65
4.3$すべてを結集オブジェクトバリデータ
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 66
4.3$すべてを結集オブジェクトバリデータ• 任意の基準に基いて妥当性を検証する
• 流暢なものがいい
• パーツから構成したい
• 発生したエラーを全て報告して欲しい
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 67
関数!checker
• 真偽値を返す関数を引数にとる
• false"が返ってきたら、エラーメッセージを追加する
• 真偽値を返す関数の"message"フィールドを探す
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 68
関数!checker!の実装function checker(/* validators */) { var validators = _.toArray(arguments);
return function(obj) { return _.reduce(validators, function(errs, check) { if (check(obj)) return errs; else return _.chain(errs).push(check.message).value(); }, []); };}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 69
関数!checker!の使用例• エラーメッセージが擬似メタデータとして付与された特殊用途の検証用関数
• 一般的な解決方法ではないが、使える
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 70
関数!checker!の使用例var alwaysPasses = checker(always(true), always(true));alwaysPasses({}); // => []
var fails = always(false);fails.message = "人生における過ち";var alwaysFails = checker(fails);alwaysFails({}); // => [ "人生における過ち" ]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 71
message!プロパティを与えることの問題• 検証用関数を生成する度に"message"を与えるのは面倒
• もとからある"message"プロパティを削除する可能性がある
• _message"としても良いが、今度は"_message"思い出さなければならない
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 72
関数!validator
• message"プロパティを与えることの問題を解決する高階関数function validator(message, fun) { var f = function(/* args */) { return fun.apply(fun, arguments); };
f['message'] = message; return f;}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 73
関数!validator!の使用例var gonnaFail = checker(validator("ZOMG!", always(false)));console.log(gonnaFail(100));// => [ "ZOMG!" ]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 74
検証用関数を分離して定義する利点• 検証用関数にわかりやすい名前を付与できるfunction aMap(obj) { reutrn _.isObject(obj);}var checkCommand = checker(validator("マップデータである必要があります", aMap));checkCommand({}); // => []checkCommand(42); // => [ "マップデータである必要があります" ]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 75
流暢な検証関数• 関数を返す関数に渡す引数は、返される関数の動作を「設定」するもの
• 例:#必要なキーの単純なリストは美しく流暢
• hasKeys('msg', 'type')とか
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 76
関数!hasKeys!の実装function hasKeys(/* 検証したいキーのリスト */) {
var KEYS = _.toArray(arguments); var fun = function(obj) { return _.every(KEYS, function(k) { return _.has(obj, k); }); }; fun.message = cat(["Must have values for keys:"], KEYS).join(" "); return fun;}
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 77
関数!hasKeys!の特徴• 自由変数"KEYS"を確保したクロージャが妥当性検証を行う
• hasKeys"関数は"fun"関数実行時の設定を行う
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 78
関数!hasKeys!の使用例var checkCommand = checker(validator("must be a map", aMap), hasKeys('msg', 'type'));
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 79
checkCommand!の使用例
checkCommand({msg:0"blah",0type:0"display"});//0=>0[0]checkCommand(32);//0=>0[0"マップデータである必要があります",0"Must0have0values0for0
keys:0msg0type"0]checkCommand({});//0=>0[0"Must0have0values0for0keys:0msg0type"0]
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 80
4.4#まとめ
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 81
高階関数の特徴!(再掲)
• 前提
• 第一級のデータ型である
• 次のうち、少なくともどちらか一方を満たす
• 引数として関数をとる
• 関数を戻り値として返す
KOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 82
4章"高階関数"まとめ• 関数を他の関数に渡した
• iterateUntil"などを例に、関数に関数を渡したほうが良い場合を示した
• 関数が関数を返す関数を定義した
• 予期しない値に対する防御策"fnull
• 強力な制約関数"checker,"validator,"hasKeysKOSHIKIZAWA)Atsushi)(2015402411))第二回Topotal輪読会 83