Upload
narimichi-takamura
View
256
Download
0
Embed Size (px)
Citation preview
関数合成とは• 関数と関数を組み合わせて新しい関数を生成すること
• 以下の例では"f"と"g"を組み合わせた関数"g∘f"を表す
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 5
invoker(関数を振り返る• 4#章(p107)で紹介された
• 関数を生成して返す
• オブジェクトをターゲットにしてメソッド呼び出しを行う
• オブジェクトが該当メソッドを持っていない場合は#
undefined#を返す
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 6
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); }); };};var rev = invoker('reverse', Array.prototype.reverse);_.map([[1,2,3]], rev);//=> [[3,2,1]]
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 7
ポリモーフィックな関数• ポリモーフィックな関数とは
• 与えられた引数によって異なる動作を行う
• 1つ以上の関数を引数に取り、それらの関数を#undefined#以外の値が返されるまで順番に呼び出す
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 8
construct(関数の振り返り// 2 章(p55)にて紹介された cat, construct の振り返りfunction cat() { var head = _.first(arguments); if (existy(head)) return head.concat.apply(head, _.rest(arguments)); else return [];}
// 要素と配列を引数に取り、配列の前に要素を挿入する関数function construct(head, tail) { return cat([head], _.toArray(tail));}construct(42, [1,2,3]);//=> [42, 1, 2, 3]
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 9
function dispatch(/* funs */) { var funs = _.toArray(arguments); var size = funs.length;
return function(target /*, args */) { var ret = undefined; var args = _.rest(arguments);
for (var funIndex = 0; funIndex < size; funIndex++) { var fun = funs[funIndex]; ret = fun.apply(fun, construct(target, args));
if (existy(ret)) return ret; } return ret; };}
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 10
dispatch)関数が行っていること
1.関数が格納された配列を走査
2.指定したオブジェクトでそれぞれの関数を呼び出す
3.最初に指定された実際(existy()%が%true)の値を返す
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 11
配列!or!文字列を文字列に変換する関数をつくる• 普通に書いたら下記のようになりがち
• 文字列"or"配列を引数に取る関数を定義
• 引数に与えられたデータの型や妥当性を判定
• if'else"ブロックでそれぞれのデータ型の"toString"メソッド呼び出し
→"invoker"関数と"dispatch"関数を用いると単純化できるTAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 12
invoker!関数と!dispatch!関数を同時に利用var str = dispatch(invoker('toString', Array.prototype.toString),invoker('toString', String.prototype.toString));
str("a");//=> "a"
str(_.range(10));//=> "0,1,2,3,4,5,6,7,8,9"
→"ポリモーフィックな関数であることがわかる
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 13
dispatch!関数における決まりごとの確認• 下記条件が満たされるまで関数を順番に実行する
• 与えられた配列に格納された関数がなくなる
• 実行した関数が正常な値を返す※!invoker!関数の仕様に依存するわけではない
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 14
文字列を逆順ソートする関数function stringReverse(s) { if (!_.isString(s)) return undefined; return s.split('').reverse().join('');}
stringReverse('abc');//=> "cba"
stringReverse(1);//=> undefined
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 15
逆順ソートを行うポリモーフィックな関数
stringReverse!と!Array#reverse!を組み合わせるvar polyrev = dispatch(invoker('reverse', Array.prototype.reverse),stringReverse);
polyrev([1,2,3]);//=> [3, 2, 1]
polyrev('abc');//=> "cba"
→"異なるデータ型を扱うことができたTAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 16
終了関数との合成• 下記の例では"always"が終了関数var polyrev = dispatch(invoker('reverse', Array.prototype.reverse),stringReverse);// dispatch 関数によって生成された関数も dispatch 関数の引数になることができるvar sillyReverse = dispatch(polyrev, always(42));
sillyReverse([1,2,3]);//=> [3, 2, 1]sillyReverse("abc");//=> "cba"sillyReverse(1000000);//=> 42
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 17
function performCommandHardcoded(command) { var result;
switch (command.type) { case 'notify': result = notify(command.message); break; case 'join': result = changeView(command.target); break; default: alert(command.type); }
return result;}
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 19
performCommandHardcoded!関数の利用例
引数に渡された!command!オブジェクトのフィールドを参照し、コマンド文字列の内容によって異なる関数を実行するperformCommandHardcoded({type:'notify', message: 'hi!'});// notify 関数を実行performCommandHardcoded({type:'join', message: 'waiting-room'});// changeView 関数を実行performCommandHardcoded({type:'wat'});// alert 関数を実行
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 20
dispatch!関数の利用function isa(type, action) { return function(obj) { if (type === obj.type) return action(obj); };}var performCommand = dispatch( isa('notify', function(obj) { return notify(obj.message); }), isa('join', function(obj) { return changeView(obj.target); }), function(obj) { alert(obj.type); });
performCommand!関数に渡す関数の注意点を考えてみようTAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 21
拡張性の比較• performCommandHardcoded"関数の場合
• 拡張する場合、switch"文の内部を変更する必要がある
• performCommand"関数の場合
• 別の"dispatch"関数でラッピングするだけで済む
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 22
performCommand!関数の拡張例!その!1
新たなコマンド!kill!を追加する例→!performCommand!関数をラップすることで拡張する// performCommand の拡張例var performAdminCommand = dispatch( isa('kill', function(obj) { return shutdown(obj.hostname); }), performCommand);
performCommand({type: 'kill', hostname: 'localhost'});// シャットダウンperformCommand({type: 'fail'});// alert を実行
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 23
performCommand!関数の拡張例!その!2
join!コマンドを制限する例→!既存コマンドをオーバーライドする。var performTrialUserCommand = dispatch( isa('join', function(obj) { alert("Cannot join until approved") }), performCommand);performTrialUserCommand({type: 'join', target: 'foo'});// 拒否メッセージが入った alert を実行performTrialUserCommand({type: 'notify', message: 'Hi new user'});// notify 関数を実行
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 24
関数合成のまとめ• 関数合成とは、関数を組み合わせて新しい関数を生成すること
• これまで説明した以下の内容が関数合成の本質
• 既知の方法で既存のパーツを使うことによって新たな動作を組み立てる
• 上記で組み立てた新たな動作も後にパーツとして利用できる
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 25
変数の変異は気にする必要がない• 関数型プログラミングにおいて関数は抽象の最小単位
• 関数が変数の境界をつくるのでローカル変数の状態変更は関数の外部に漏れることはない
• 変数の変異は低レイヤーにおける操作として意識の外に置くべき
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 27
命令型プログラミングとの比較// 命令型プログラミングvar result = 0;for(var n = 1; n <= 10; n++) { result = result + n;}
// 関数型プログラミングvar numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];var plus = function(a, b) { return (a + b);};var result = _.reduce(numbers, plus);
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 28
使うべき道具を理解しよう• 本書は関数型プログラミングの美徳を熱く説くものではない
• ライブラリの特性や実行速度などの都合により、命令型プログラミングで実装すべき場面もある
• 問題とその解決策を理解する力とそこで使える引き出しを持っていることが、最善のソリューションにつながる
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 29
数学的に捉えてみる
!は!2!引数関数であり、左から順に引数として! ,! !を受け取る
これを左からカリー化すると...
このとき、関数! !は!関数! !はカリー化された関数という。
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 32
カリー化の具体例function leftCurryDiv(n) { return function(d) { return n/d; };}function rightCurryDiv(d) { return function(n) { return n/d; };}var divide10By = leftCurryDiv(10);divide10By(2)//=> 5var divideBy10 = rightCurryDiv(10);divideBy10(2)//=> 0.2
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 35
固定された関数を返す高階関数!curry
• divide10By"と"divideBy10"は手動だった
• 今回は引数を"1"つだけ取るように固定された関数を返す関数でカリー化を行う
function curry(fun) { return function(arg) { return fun(arg); };}
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 37
parseInt
• parseInt:#文字列を引数にとり、それを数値に変換する
• 第#1#引数:#文字列
• 第#2#引数:#底parseInt('11');//=> 11parseInt('11', 2);//=> 3
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 38
parseInt!+!Array#map!で発生する問題• それぞれの配列要素に対して引数に与えられた関数を実行
• 実行時、要素・インデックス・元の配列が与えられる
• 引数を"1"つだけ取るように矯正すれば問題ない> ['11','11','11','11'].map(parseInt);//=> [ 11, NaN, 3, 4 ]> ['11','11','11','11'].map(curry(parseInt));//=> [ 11, 11, 11, 11 ]
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 39
2"つのパラメータをカリー化function curry2(fun) { return function(secondArg) { return function(firstArg) { return fun(firstArg, secondArg); }; };}var parseBinaryString = curry2(parseInt)(2);
parseBinaryString("111");//=> 7parseBinaryString("10");//=> 2
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 40
コラム:"なぜ右からカリー化?
• 右のほうにある引数は専門化のためのオプションであることが多い
• parseInt"もこれに当てはまる
• これを固定化(または無視)することで関数の動作をコントロールしやすくなる
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 41
var plays = [{artist: "Burial", track: "Archangel"}, {artist: "Ben Frost", track: "Stomp"}, {artist: "Ben Frost", track: "Stomp"}, {artist: "Burial", track: "Archangel"}, {artist: "Emeralds", track: "Snores"}, {artist: "Burial", track: "Archangel"}];
_.countBy(plays, function(song) { return [song.artist, song.track].join(" - ");});//=> {"Ben Frost - Stomp": 2,// "Burial - Archangel": 3,// "Emeralds - Snores": 1}
function songToString(song) { return [song.artist, song.track].join(" - ");}
var songCount = curry2(_.countBy)(songToString);songCount(plays);//=> {"Ben Frost - Stomp": 2,// "Burial - Archangel": 3,// "Emeralds - Snores": 1}
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 43
_.uniq
• 配列から重複要素を取り除いた新しい配列を返す
• _.uniq(array, [isSorted], [iterator])
• isSorted"はすでに配列がソート済みの場合に用いる
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 45
3"段階のカリー化function curry3(fun) { return function(last) { return function(middle) { return function(first) { return fun(first, middle, last); }; }; };};var songsPlayed = curry3(_.uniq)(false)(songToString);songsPlayed(plays);//=> [{artist: "Burial", track: "Archangel"},// {artist: "Ben Frost", track: "Stomp"},// {artist: "Emeralds", track: "Snores"}]
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 46
HTML%カラーコードを生成する関数function toHex(n) { var hex = n.toString(16); return (hex.length < 2) ? [0, hex].join(''): hex;}function rgbToHexString(r, g, b) { return ['#', toHex(r), toHex(g), toHex(b)].join('');}rgbToHexString(255, 255, 255);//=> "#ffffff"
var blueGreenish = curry3(rgbToHexString)(255)(200);blueGreenish(0);//=> "#00c8ff"
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 47
5.3.3$「流暢な」APIのためのカリー化• Haskell(では、関数はデフォルトでカリー化されている
• 一方、JavaScript(ではそうではない。そのため、API(やドキュメントを用意する必要がある
• カリー化を用いるかどうかの一般的なルールは「API(が高階関数を活用するか」
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 48
5.3.4%JavaScript%におけるカリー化のデメリット• 任意の段階までカリー化する関数は実用的ではない
• Haskell(や(Shen(では多段カリー化を有効活用できる(API(がある
• JavaScript(では一般的にカリー化は不利に働き、混乱を招く
• 可変数引数が許可されているため
• カリー化よりも任意の深さまでの部分適用がより一般的に使われる
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 49
部分適用とは• 関数を"任意"の数だけ引数を指定して部分的に実行する
• 部分適用された関数は残りの引数を与えると即時実行される
• f(a,b,c)"→"g(a,b)
• a,b"を付与すると即時実行
• カリー化は"h(a)"のように引数を1つにする
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 51
1つの既知の引数を部分適用function div(n, d) { return n / d }
function partial1(fun, arg1) { return function(/* args */) { var args = construct(arg1, arguments); return fun.apply(fun, args); };}
var over10Part1 = partial1(div, 10);over10Part1(5);//=> 2
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 53
2つの既知の引数を部分適用function partial2(fun, arg1, arg2) { return function(/* args */) { var args = cat([arg1, arg2], arguments); return fun.apply(fun, args); };}var div10By2 = partial2(div, 10, 2);div10By2()//=> 5
→"1つか2つの引数の部分適用はよくある。任意の数の引数を予め適用できればさらに便利になる。TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 54
function partial(fun /*, 任意の数の引数*/) { var pargs = _.rest(arguments);
return function(/* arguments */) { var args = cat(pargs, _.toArray(arguments)); return fun.apply(fun, args); };}
var over10Partial = partial(div, 10);over10Partial(2);//=> 5
var div10By2By4By5000Partial = partial(div, 10, 2, 4, 5000);div10By2By4By5000Partial();//=> 5
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 56
validator!関数を思い出してみよう• 検証用のプレディケート関数を引数に取る
• エラーが発生した場合のエラーメッセージを関数のオブジェクトフィールドに格納して返す関数
function validator(message, fun) { var f = function(/* args */) { return fun.apply(fun, arguments); };
f['message'] = message; return f;}
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 58
validator!関数の利用例var zero = validator("0 ではいけません", function(n) { return 0 === n; });var number = validator(" 引数は数値である必要があります", _.isNumber);
function sqr(n) { if (!number(n)) throw new Error(number.message); if (zero(n)) throw new Error(zero.message); return n * n;}sqr(10);//=> 100sqr(0);// Error: 0 ではいけませんsqr('');// Error: 引数は数値である必要があります
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 59
事前条件と事後条件• 事前条件
• 関数の呼び出し元の保証
• 例示したような入力データ検証(zero,#number)
• 事後条件
• 事前条件が満たされたと想定した場合の、関数呼び出しの結果に対する保証
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 60
事前条件と計算の骨子を区別する• zero"と"number"という2つの事前条件は計算自体の骨子に関係しない
• 実行部の動作保証を行うだけなので分離すべき→事前条件と計算の骨子を分離し、そのあとで新たな関数を用いてそれらを部分適用を行うことで結びつける
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 61
// 具体的な事前条件を引数に取るfunction condition1(/* validators */) { var validators = _.toArray(arguments);
return function(fun, arg) { var errors = mapcat(function(isValid) { return isValid(arg) ? [] : [isValid.message]; }, validators);
if (!_.isEmpty(errors)) throw new Error(errors.join(", "));
return fun(arg); };}
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 62
condi&on1(の利用例var sqrPre = condition1( validator("0 ではいけません", complement(zero)), validator("引数は数値である必要があります", _.isNumber));
sqrPre(_.identity, 10);//=> 10sqrPre(_.identity, '');// Error: 引数は数値である必要がありますsqrPre(_.identity, 0);// Error: 0 ではいけませんTAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 63
安全でない!sqr!関数の例function uncheckedSqr(n) { return n * n };uncheckedSqr('');//=> 0
• JavaScript+は演算時に空の文字列を+0+に自動変換する
• 空の文字列の2乗が+0+であることを許容すべきではない→"これを解決するために"validator,"partial1,"condition1,"
sqrPre"を組み合わせるTAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 64
おさらい!1
// エラーが発生した場合のエラーメッセージを関数のオブジェクトフィールドに格納して返すfunction validator(message, fun) { var f = function(/* args */) { return fun.apply(fun, arguments); };
f['message'] = message; return f;}// 1つの既知の引数を部分適用function partial1(fun, arg1) { return function(/* args */) { var args = construct(arg1, arguments); return fun.apply(fun, args); };}
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 65
おさらい!2
// 事前条件と計算の骨子を分離するfunction condition1(/* validators */) { var validators = _.toArray(arguments);
return function(fun, arg) { var errors = mapcat(function(isValid) { return isValid(arg) ? [] : [isValid.message]; }, validators);
if (!_.isEmpty(errors)) throw new Error(errors.join(", "));
return fun(arg); };}// 事前条件と計算の骨子を結びつけるvar sqrPre = condition1( validator("0 ではいけません", complement(zero)), validator("引数は数値である必要があります", _.isNumber));
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 66
引数の妥当性チェックを計算から分離するvar checkedSqr = partial1(sqrPre, uncheckedSqr);
checkedSqr(10);//=> 100checkedSqr('');// Error: 引数は数値である必要がありますcheckedSqr(0);// Error: 0 ではいけません
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 67
更に追加の検証項目を加えるvar sillySquare = partial1(condition1(validator("偶数を入力してください", isEven)), checkedSqr);
sillySquare(10);//=> 100sillySquare(11);// Error: 偶数を入力してくださいsillySquare('');// Error: 引数は数値である必要がありますsillySquare(0);// Error: 0 ではいけません
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 68
関数の生成時のカリー化および部分適用の制約• 「1#つ以上の数の引数を専門化することによって合成する」という共通の制約
• 引数と戻り値の関係を関数合成に持ち込みたい場合がある→"関数を並べて端から端までつなぎ合わせる"_.compose"関数を紹介する
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 69
関数のつなぎあわせfunction isntString(str) { return !_.isString(str);}isntString(1);//=> true
// compose 関数によるつなぎあわせ(右から左に実行される)var isntString = _.compose(function(x) { return !x }, _.isString);isntString([]);//=> true
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 71
前節のおさらいと本節の目標• 前節のおさらい
• 引数が前提に準拠するかを確認してから二乗する関数"
checkedSqr"を組み立てた
• 本節の目標
• 事後条件(関数呼び出しの結果に対する保証)を"
checkedSqr"に追加するTAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 73
事後条件の定義var sqrPost = condition1(validator("結果は数値である必要があります", _.isNumber),
validator("結果はゼロではない必要があります", complement(zero)),validator("結果は正の数である必要があります", greaterThan(0)));
sqrPost(_.identity, 0);// Error: 結果はゼロではない必要があります, 結果は正の数である必要がありますsqrPost(_.identity, -1);// Error: 結果は正の数である必要がありますsqrPost(_.identity, '');// 結果は数値である必要があります, 結果は正の数である必要がありますsqrPost(_.identity, 100);//=> 100
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 74
事後条件を既存の関数に追加する
_.compose!を接着剤として利用するvar megaCheckedSqr = _.compose(partial(sqrPost, _.identity), checkedSqr);
// 事前条件の検証時エラーmegaCheckedSqr(10);//=> 100megaCheckedSqr(0);// Error: 0 ではいけません
// 事後条件の検証時エラーmegaCheckedSqr(NaN);// Error: 結果は正の数である必要があります
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 75
事後条件でのエラーは常に自らの失敗• 事後条件の検証時エラーは以下のようなときに発生する
• 事前条件に漏れがある
• 事後条件が厳しすぎる
• 内部ロジックに不具合がある→"自らの失敗により発生することがほとんどである
TAKAMURA'Narimichi'/'第'4'回'Topotal'輪読会@2015/03/18 76