Pluralsightの「Hardcore Functional Programming in JavaScript」(Brian Lonsdorf)学習メモ。
レベルはAdvancedとあるが、実際はJavaScriptでの関数型プログラミング・初中級編といった内容だった。練習問題が豊富でなかなか楽しめた。
- Tips
- 不要な名前は削除する - 例:関数内にdate()がある=隠れたInputが存在する
- 変形と計算を分ける - DOM操作と計算ロジックは分離する。
- 純粋関数の重要性 - Testable、Memoizable、Portable、Parallelizable
- 関数と規則(rule)を分離
- 引数の個数と関数を分離
- ほとんどのループは、reduce/filter/mapのいずれかにできる
- カリー化の練習問題(Ramda.jsをアンダースコアに適用。以下すべて同様)
- 文字列を単語の配列に変更する関数wordを作る。ただし_.splitのみを使うこと
var words = _.split(' ');
- 配列のメンバをすべて3倍する関数tripleListを作る。ただし、_.multiplyと_.mapのみを使うこと
var tripleList = _.map(_.multiply(3));
- 配列から最大値を探す関数maxを作る。ただし、2引数の最大値を返す関数greater、および_.map、_.filterまたは_.reduceのみを使うこと
var max = _.reduce(greater, -Infinity);
- 関数合成の練習問題
- articlesからauthor名のリストnamesを作成する。ただし、get(オブジェクトからプロパティを取得する関数)、_.compose、_.mapのみを使うこと
var names = _.map(_.compose(get('name'), get('author')));
- いずれかのauthorになっているか判定する関数isAuthorを作成する。ただし、上記の関数に加え、_.compose、_.containsのみを使うこと
var isAuthor = function(name, articles) { return _.compose(_.contains(name), names)(articles); };
- リストの平均値を返す関数avgを作成する。
var fork = _.curry(function(lastly, f, g, x) {
ただし、上記forkと_.divide、_.sum、_.sizeのみを使うこと。
return lastly(f(x), g(x));
});
var avg = fork(_.divide, _.sum, _.size);
- Point-freeスタイル
引数を使わないスタイル。なぜかPointは引数の意味で使われる。前の練習問題3回答が実例。
var avg = fork(_.divide, _.sum, _.size);
メリットがあまり語られなかったので、参考リンク:
http://madscientist.jp/~ikegami/articles/PointFreeStyleWhatIsGoodFor.html
このサイトによると、(1)問題が小さくなること(2)ボトルネックが見つけやすくなること、がメリットらしい。デメリットは可読性が落ちること。
- 圏論
compose:: (b -> c) -> (a -> b) -> (a -> c) id:: a -> a
-
left identity
compose(id, f) == f
-
right identity
compose(f, id) == f
-
associativity
compose(compose(f, g), h) == compose(f, compose(g, h))
オブジェクトを次のような、単純なContainerとmap関数と考える。
var _Container = function(val) { this.val = val; }; _Container.prototype.map = function(f) { return Container(f(this.val)); }; var Container = function(x) { return new _Container(x); };
すると、ドットで連結して表現できるようになる。途中で型が変わっても問題ない。
alert(Container(3).map(_.add(1)).val); //4 alert(Container([1,2,3]).map(_.reverse).map(_.head).val); //3 alert(Container("flamethrower").map(_.length).map(_.add(1)).val); //13
さらにmap関数を定義すると、部分適用に対応できるようになる。
var map = _.curry(function(f, obj) { return obj.map(f); });
下記2つは同じ結果だが、2番目のコードはContainerオブジェクトが無くともmap関数が使える。
alert(Container(3).map(_.add(1)).val); //4 alert(map(_.add(1), Container(3)).val); //4
その他の使用例。
alert(map(_.match(/cat/g), Container("catsup")).val); //cat alert(map(_.compose(_.head, _.reverse), Container("dog")).val); //g
- Maybeファンクター
ファンクターは「map(写像、map over)できるオブジェクトまたはデータ構造」と定義する。動画では明言してなかったが、上記Containerも当然ファンクタ―になる。Maybeファンクタ―は、このContainerにNullチェックを加えたもの。
var _Maybe = function(val) { this.val = val; }; _Maybe.prototype.map = function(f) { return this.val ? Maybe(f(this.val)) : Maybe(null); }; var Maybe = function(x) { return new _Maybe(x); }; var map = _.curry(function(f, obj) { return obj.map(f); }); alert(map(_.match(/cat/g), Maybe("catsup")).val); //cat alert(map(_.match(/cat/g), Maybe(null)).val); //null
_.composeと組み合わせての使用例。Maybeは関数なので_.composeの引数にできる。
var firstMatch = _.compose(map(_.head), Maybe, _.match(/cat/g)); alert(firstMatch("dogsup").val); //null
- Maybeファンクター練習問題
- ファンクター内の値をインクリメントする関数を作成する。ただし_.add(x,y)とmap(f,x)を使うこと
var ex1 = map(_.add(1));
- ファンクター内の値(リスト)から、先頭の値を取り出す関数を作成する
var ex2 = map(_.head);
- Userオブジェクトのnameからイニシャルを取り出す関数を作成する。ただし次のsafeGetと_.headを使用すること
var safeGet = _.curry(function(x,o){ return Maybe(o[x]); });
var ex3 = _.compose(map(_.head), safeGet('name'));
- 次の関数をMaybeファンクターを使って書き直す
var ex4_org = function(n) { if(n){ return parseInt(n); } };
var ex4 = _.compose(map(parseInt), Maybe);
- Eitherファンクター
Maybeファンクターと似ているが、エラーメッセージを埋め込んで主にエラーハンドリングに使われる。
Left/Rightのサブクラス(JavaScriptにはクラスがないが、便宜上クラスベース言語の用語を使ったようだ)を持つ。関数はRightへはmapされるが、Leftにmapしようとしても無視される。
alert(map(function(x) { return x + 1; }, Right(2)).val); //3 alert(map(function(x) { return x + 1; }, Left('some message')).val); //some message
var determineAge = function(user) { return user.age ? Right(user.age) : Left("couldn't get age"); } var yearOlder = _.compose(map(_.add(1)), determineAge); alert(yearOlder({age:22}).val); //23 alert(yearOlder({age:null}).val); //couldn't get age
MaybeはFalsyになった時点でNullを保持して戻ってくるが、Eitherはその代わりにエラーメッセージを持ってくるイメージ。
※Left・Rightの実装は以下(動画では割愛されていた)
https://frontendmasters.com/assets/resources/functionaljs/data.either.umd.js
- IOファンクター
文字列などの値ではなく、関数を保持する。副作用のある処理に使われる。
関数をmapすると、内部のリストに対象関数が追加される。処理を走らせるためには、明示的にrunIOを実行する必要がある(PromiseのLazy版に近い)。
var email_io = IO(function() { return $("#email").val(); }); var msg_io = map(concat("welcome "), email_io); alert(runIO(msg_io)); //"welcome someone@somedomain.com"
IO()に加えて、セレクタを引数化したヘルパ関数を用意すると便利。
var getValue = function(sel) { return $(sel).val(); }.toIO();
※IOの実装は以下(動画では割愛されていた)
https://frontendmasters.com/assets/resources/functionaljs/io.js
- Either/IOファンクター練習問題
- Welcomeメッセージ(Right)またはエラー(Left)を返す関数を作成する。エラー判定にはcheckActive、Welcomeメッセージ作成にはshowWelcomeを使用すること
var showWelcome = compose(_.add( "Welcome "), _.get('name')) var checkActive = function(user) { return user.active ? Right(user) : Left('Your account is not active') }
var ex1 = _.compose(map(showWelcome), checkActive);
- 引数が3以上かチェックするバリデート関数を作成する。OKの場合はRight、Errorの場合はメッセージ付きでLeftを返すこと
var ex2 = function(x) { return x.length > 3 ? Right(x) : Left("You need > 3"); }
- 問題2とEitherファンクタ―を使い、OKの場合は対象をsaveする関数を作成する。saveには以下の関数を使用すること
var save = function(x){ console.log("SAVED USER!"); return x; }
var ex3 = _.compose(map(save), ex2)
- テキストから空白を削除する関数を作成する
var getValue = function(x){ return document.querySelector(x).value }.toIO() var stripSpaces = function(s){ return s.replace(/\s+/g, ''); }
ただし、テキストを取得する関数getValueはIOである。
var ex4 = _.compose(map(stripSpaces), getValue);
- ページのプロトコルを取得する関数を作成する
var getHref = function(){ return location.href; }.toIO(); var getProtocal = compose(_.head, _.split('/'))
ただし上記関数を使用すること。
var ex5 = _.compose(map(getProtocal), getHref);
- getCacheから取得したUserを使い、Maybe(email)を返す関数を作成する
localStorage.user = JSON.stringify({email: "george@foreman.net"}) var getCache = function(x){ return Maybe(localStorage[x]); }.toIO();
JSONからオブジェクトを作るのにJSON.parseが必要になるのを忘れないこと。
var ex6 = _.compose(map(map(_.compose(_.prop('email'), JSON.parse))), getCache);
模範解答は上記。だが、IOではないMaybe版を作成して、最後にまとめてtoIO()をかけたほうが分かりやすい気がするのだが、どうだろうか?
var getCache_noIO = function(x){ return Maybe(localStorage[x]); };
var ex6_noIO = _.compose(map(_.prop('email')), map(JSON.parse), getCache_noIO); var ex6_noIO_plusIO = function(x) { return ex6_noIO(x); }.toIO();
このほうが、IO化/非IO化が簡単に切り替えられてよさそうだ(getCacheを改変してるので、完全に正しい答えではないが)。フォーラムで聞いてみることにする。
- EventStreamファンクタ―
mapされている関数を、イベントが起こるたびに実行する。addEventListener()の仕事に似ているが、ファンクターなので関数のmapを繰り返せる。結果は可変長リストに保存される。関数のmapはLazyに行われることもある。
有名な実装にBacon.jsがある。
https://baconjs.github.io/api.html#eventstream
var id_s = map(function(e) { return '#' + e.id; }, Bacon.fromEvent(document, "click")); id_s.onValue(function(id){ alert('you clicked ' + id); })
※動画ではBacon.fromEventTarget()だが、これは古い記法か
1行目では関数をmapしているのみで、実行はされない。2行目のonValueでイベントのリッスンが開始する。
var id_s = map(function(e) { return '#' + e.id; }, Bacon.fromEventTarget(document, "click")); var element_s = map(document.querySelector, id_s); element_s.onValue((function(el){ alert('The inner html is ' + el.innerHTML); }));
id_sはEventStreamファンクターなので、続けて関数をmapし別のEventStreamを作成することができる。
- Futureファンクタ―
文字列などではなく、関数を値として持つ。JQueryのPromiseに似ているがLazy。開始するにはforkする必要がある。
※実装は以下(動画では割愛されていた)
http://looprecur.com/hostedjs/data.future.umd.js
var makeHtml = function(post) { return "<div>" + post.title + "/<div>" }; var page_f = map(makeHtml. http.get('/posts/2')); page_f.fork(function(err{ throw(err); }, function(page){ $('#container').html(page); });
http.get()がFutureファクターを返す(この関数はPseudo)。
- EventStream/Futureファンクター練習問題
- Postからtitleを取得するFutureファンクターを作成する。ただしPostの取得には次の関数を使うこと
function getPost(i) { return new Future(function(rej, res) { setTimeout(function(){ res({id: i, title: 'Love them futures'}) }, 300) }) }
var ex1 = _.compose(map(_.prop('title')), getPost);
getPostは300ms後Postオブジェクトを引数にセットし、mapされた関数を実行している。
ex1(3).fork(log, function(title){ assertEqual('Love them futures', title) console.log("exercise 1..ok!") })
forkの使用例。 - 問題1のex1を、次の関数でtitleをレンダリングするように拡張する。
var render = function(x){ return "<div>"+x+"</div>"; }
var ex2 = _.compose(map(render), ex1);
- 次のEventStreamファンクターclicksを拡張し、ターゲットのinnerHTMLを取得するようにする
var clicks = Bacon.fromEventTarget(document.querySelector("#box"), "click")
var htmlClicks = clicks.map(function(e) { return e.target.innerHTML; });
- (問題4は割愛)
- safeGetのみを使い、userからstreet nameを取得するMaybeファンクターを作成する
var safeGet = _.curry(function(x,o){ return Maybe(o[x]) }) var user = {id: 2, name: "Albert", address: { street: {number: 22, name: 'Walnut St'} } }
var ex5 = _.compose(map(map(safeGet('name'))), map(safeGet('street')), safeGet('address'));
期待値はMaybeのネストになる。
assertDeepEqual(Maybe(Maybe(Maybe('Walnut St'))), ex5(user))
- ファンクターの法則とプロパティ
ライブラリによって、mapは様々な名前が付いている(例:map、attempt、then、subscribe)。しかし、これらはすべて数学的に同等。
ファンクターの法則:
- map(id) == id
- compose(map(f), map(g)) == map(compose(f, g))
var reverse = function(s) { return s.split("").reverse().join("");; }; var toArray = function(x) { return [x]; }; var toUpper = function(s) { return s.toUpperCase(); }; //same results alert(_.compose(toArray, reverse)("bingo")); //[ognib] alert(_.compose(map(reverse), toArray)("bingo")); //[ognib] //same results alert(_.compose(toArray, compose(toUpper, reverse))("bingo")); //[OGNIB] alert(_.compose(map(compose(toUpper, reverse)), toArray)("bingo")); //[OGNIB] alert(_.compose(map(toUpper), map(reverse), toArray)("bingo")); //[OGNIB]
このあたりの正確な理解には圏論の勉強が必要そうだ。
- Pointedファンクター
mapメソッドとofメソッド(ほかにpure、return、unit、pointなどと呼ばれる)を持つすべてのファンクター。ofは引数を該当するファンクターに保持して返す関数。
ofには何でも渡せるので、コンストラクタ関数を使うより優れた方法になる。たとえば、EventStreamコンストラクタには関数は渡せないが、ofメソッドを使えば可能である。
Container.of(split); //Container(split) Future.of(_.match(/dubstep/)); //Future(_.match(/dubstep/)); Maybe.of(reverse); //Maybe(reverse) EventStream.of(_.replace(/dubstep/, 'shoegaze')); //EventStream(_.replace(/dubstep/, 'shoegaze'));
※上記はPseudoコード
文脈(Context)について
ここで動画では唐突に文脈という用語が現れる。次のページの説明が分かりやすかったので引用しておく。
http://walk.wgag.net/haskell/monad.html
Functor クラスは値を入れるコンテナのような構造を表したものと紹介しました。
言い方を変えると,Functor クラスは値に特定の文脈 (context) を付加する構造だと言えます。
例えば,Maybe は "失敗可能性" という文脈,IO は "副作用演算" という文脈を値に付加します。
- モナド
Pointedファンクターに、関数mjoin、chain(いずれか、または両方)が付加されたもの。Maybe、IO、EventStreamなどもモナド。
mjoin :: M M a -> M a chain :: (a -> M b) -> M a -> M b
mjoin:Containerのネストを引数にとり、1段のContainerを返す。それぞれのContainerが同じ型であることに注意
chain:値aをbのContainerに変える関数と、aのContainerを引数にとり、bのContainerを返す。flatMapやbindと名付けられることもある。次のように定義できる。
var chain = function(f) { return compose(mjoin, map(f)); }; var mjoin = chain(id);
mjoinの使用例。3行目でMaybeのネストになるところを、mjoinを足してフラットにしている。
var getTrackingId = _.compose(Maybe, get("tracking_id")); var findOrder = _.compose(Maybe, Api.findOrder); var getOrderTracking = _.compose(mjoin, map(getTrackingId), findOrder); var renderPage = _.compose(map(renderTemplate), getOrderTracking); renderPage(379); //Maybe(Html)
chainの使用例。次のmjoinとmapが連続している部分は、
var sendToServe = httpGet('/upload'); var uploadFromFile = compose(mjoin, map(sendToServer), mjoin, map(readFile), askUser); uploadFromFile('what file?').fork(logErr, alertSuccess);
以下のように書くことができる。
var sendToServe = httpGet('/upload'); var uploadFromFile = _.compose(chain(sendToServer), chain(readFile), askUser); uploadFromFile('what file?').fork(logErr, alertSuccess);
さらに次のように書き換えられ、文字通りchainすることができる。
var sendToServe = httpGet('/upload'); var uploadFromFile = function(what) { return askUser(what).chain(readFile).chain(sendToServer); }; uploadFromFile('what file?').fork(logErr, alertSuccess);
- モナドの練習問題
- safeGetとmjoinまたはchainを使い、安全にstreet nameを取得する
var safeGet = _.curry(function(x,o){ return Maybe(o[x]) })
var ex1_1 = _.compose(mjoin, map(safeGet('name')), mjoin, map(safeGet('street')), safeGet('address')); var ex1_2 = _.compose(chain(safeGet('name')), chain(safeGet('street')), safeGet('address'));
- hrefをモナドを使って取得しlogに書き出す
var getHref = function(){ return location.href }.toIO(); var pureLog = function(x){ console.log(x); return x; }.toIO();
模範解答は下記だが、
var ex2 = _.compose(chain(pureLog), getHref);
次のように書いたほうがコンパクトだと思う。
var ex2 = pureLog.chain(getHref);
- モナドを使い、まずgetPostでPostを取得し、そのidをgetCommentsに渡す
function getPost(i) { return new Future(function(rej, res) { setTimeout(function(){ res({id: i, title: 'Love them futures'}) }, 300) }) } function getComments(i) { return new Future(function(rej, res) { setTimeout(function(){ res(["This class should be illegal", "Monads are like space burritos"]) }, 300) }) }
模範解答は以下。モナドどうしのcomposeを簡潔にするmcomposeも紹介されていたが、どうもこの関数は提供されていないようだ。
var ex3 = compose(chain(getComponents), getPost); var ex3 = mcompose(getComments, getPost); //mcompose not provided through
しかし、このコードはidの取得を忘れているので、次のように書くべきだと考える。
var ex3 = _.compose(chain(getComments), map(_.prop('id')), getPost);
- ライブラリ
JavaScript向けの関数型プログラミングライブラリは、まだ発展途上。
・Ramda(もっとも揃っている)
・bacon.js
・fantasy-io(これはスペック)
・pointfree-fantasy
・data.either
などから取捨選択して利用することになる。
- デモ
入力テキストから、Youtubeのビデオをサーチして一覧表示し、クリックしたものを再生するサイトのデモ。scripts/app.jsで学んだテクニックを使っている。