『JavaScriptで学ぶ関数型プログラミング』を読んだ 01
関数型プログラミングを学ぶということはそれ自体がゴールであるべきではなく、ゴールに到達するためのテクニックであるべきです
本書 292p.
JavaScriptで関数型プログラミングがどこまでできるのか、またどうやったらできるのかを、あらためて認識したいと思い本書を手に取りました。
本書はJavaScriptというある意味クセのある言語で、関数型プログラミングをどう行うことができるのか具体的なコードとともに紹介しています。少し古い(2014年発行)ですが、ほとんどがVanillaなJavaScriptを使っての解説だったためあまり問題にはなりませんでした。
一部Underscore.js
というライブラリを使用して説明されていますが、これはあくまで関数型プログラミングを実践するための補助輪のようなものなので、本質的な理解の障害となることはありませんでした。
むしろ、シンプルな補助輪のみの状況でJavaScriptを使って再帰関数や高階関数などの複雑な関数を書いていくため、ひととおりコードを書き終えると自分の実装力が上がったような気がしました。
解説の流れとしては、JavaScriptにおけるthis参照の複雑さやクロージャの利用法などJavaScriptが持つ基本的な特性の解説から始まり、最終的にはモナドやパイプラインといった概念を、JavaScriptの実際に動くコードを書くことによって理解を深めていくといった形式です。
加えて、再帰とJavaScriptのスタックについての説明や、カリー化と部分適用の実際の利用方法など、JavaScriptで関数型プログラミングを行うにあたって現実的に考慮すべき制約や限界も詳しく解説されています。
実際にスクラッチで書くコードによって徐々に関数型プログラミングの本丸に近づいていくので、内容としては複雑なものの違和感なく自然に進められました。
ただ、『関数型プログラミングの基礎』を読んだでも書きましたが、本書にも再帰関数・高階関数・合成された関数が数多く出てくるので、『なっとく関数型プログラミング』や『なっとくアルゴリズム』を読んでいた方がウッとならずに理解も早い気がします。
JavaScriptでの関数型プログラミングを解説している書籍としては他に、『関数型プログラミングの基礎』があります。
私は先にこちらを読んでから本書に進みました。ただ、上記書籍は関数型言語そのものをJavaScriptで実装するとどうなるか(代入を一切使わずにリストを作るなど)という内容であり、関数型言語自体についてさらに一歩進んで深掘りする内容でした。
本書はどちらかというと関数型言語を利用する側から、関数型プログラミングでどうやってソフトウェアを実装していくかという観点で解説されていました。
また、上記書籍の解説は本書と同じような流れだったので、もしかすると本書で行われていたパイプライン概念のJavaScriptでの実装といった、関数型プログラミングで頻繁に使われる仕組みを実際に手元で実装してみるという試みを、さらに押し進めたのが上記の『関数型プログラミングの基礎』だったのかなと、本書を読んでいて思いました。
もしかしたら本書を先に読んでから上記の書籍に進んだ方が理解がスムースだったかもしれません。
読み終えた感想としては、日頃、主としてJavaScriptを使用しているエンジニアが、どうやってJavaScriptを使用しながら関数型プログラミングをやっていくかの理解を深めるのに最適な本だと思いました。
少し古いですが、読む価値があるとてもいい本でした。
Jeremy Ashkenasによるまえがき
- この本はとてもエキサイティングな本
- JavaScriptは「Java Lite」的スクリプト言語が出自
- HTMLドキュメントにインラインで埋め込むことで、インタラクティブ性を実現することだけを目的に作られた
- しかしもっとも柔軟なプログラミング言語であり続けている
- JavaScriptを使ってスケッチし、にじませて、ちょっとしたコードのドラフトを書くことができる
- 他の硬い言語と違って自然にこれができる
- JavaScriptが「すべてはオブジェクトである」を徹底して追求しているから
- 関数はオブジェクトであり、関数は値
- JavaScriptは「Java Lite」的スクリプト言語が出自
Steve Vinoskiによるまえがき
- 関数型プログラミングはコンピュータが出現した時から存在した
- だが、実務プログラマーの間では注目を集めることはなく、成長することもなかった
- ここにきて人気が急激に高まっている
訳者まえがき
- なぜJavaScriptで無理やり関数型プログラミングを行うのか
- 7章に理由がある
- 関数に純粋性を持たせることによってテスト性を高める
- 不純なものを可能な限り隔離することでシステム全体のテスト性を高める
- これらによって、ソフトウェアの複雑さ最小限に抑える
- 関数型言語としてJavaScriptを理解し使用することによって、コードを洗練し読みやすくする
- 7章に理由がある
- 本書の前半では、上記を実現するために必要となる関数型プログラミングの技術や考え方を解説している
- 8章以降では、実際のJavaScriptコーディングに関数型プログラミングをどのように混ぜ込むことができるかを解説している
はじめに
- 本書は誰のために書かれたものか
- JavaScriptにおいて可能であることと不可能なことを念頭に置いた関数型プログラミングの入門書として執筆した
- 関数型プログラミングを学びたいと思うJavaScript経験者あるいはその逆の人
- 本書のロードマップ
- 1章: 関数型JavaScriptへのいざない
- 2章: 第一級関数と作用的プログラミング
- 作用的プログラミングと呼ばれる第一級関数を使ったテクニック
- 関数型プログラミングを中心としたソフトウェア開発のアプローチ「データ思考」の説明
- 3章: JavaScriptにおける変数のスコープとクロージャ
- 4章: 高階関数
- 5章: 関数を組み立てる関数
- 関数合成の方法
- 6章: 再帰
- 7章: 純粋性、不変性、変更方針
- 8章: フローベースプログラミング
- タスクやシステム全体を、関数によりデータを変換する仮想の組み立てラインとして実現する
- 9章: クラスを使わないプログラミング
1章: 関数型JavaScriptへのいざない
- 関数型JavaScriptへのいざない
JavaScriptに関する事実
- JavaScriptに関する事実
- JavaScriptについて
- なぜ開発言語としてJavaScriptを選ぶのか
- それはリーチの大きさ
- JavaScriptの柔軟性
-
apply関数の例
function splat(fun) { return function (array) { return fun.apply(null, array); }; } var addArrayElements = splat(function (x, y) { return x + y; }); addArrayElements([1, 2]);
- すべての関数内でargumentsというローカル変数にアクセスできる
-
- なぜ開発言語としてJavaScriptを選ぶのか
- JavaScriptの制限
- JavaScriptは欠陥を持った言語であるということを認めておかなければならない
- 言語の妙な癖、安全ではない機能、競合するライブラリの山
- これを超えていくアプローチの一つとして関数型プログラミングがある
- JavaScriptは欠陥を持った言語であるということを認めておかなければならない
- JavaScriptについて
関数型プログラミングを始めるために
- 関数型プログラミングを始めるために
- なぜ関数型プログラミングが必要なのか
- オブジェクト指向プログラミングのゴールとは、問題をパーツに分解していくことである
- 問題を名詞のグループに分解していく
- オブジェクト間の作用は内部的な変更を伴い、状態の変更の集合体としてシステム全体を状態を表現する
- 混乱することがある
- 関数型プログラミングは問題を関数のパーツに分解していく
- 問題を動詞のグループに分解していく
- 複数の小さい関数を糊付け、または合成していく
- 与えられた値を異なる値に徐々に変換していく
- 原材料を入れると徐々に製品を組み立てていく組み立てラインの機械のようなもの
- 状態変化を最小限に抑えるため、関数がどのよう動作するのかを理解することのみが求められる
- しかし、FPとOOPは対立しない
- 共存できるかつ共存すべき
- オブジェクト指向プログラミングのゴールとは、問題をパーツに分解していくことである
- 抽象単位としての関数
- 抽象化を行う方法
- 「実行させ、正しく実行させ、速く実行させろ」 --- Butler Lampson (UNIXコミュニティの格言)
- Kent BeckのTDDを満たすことができる
- 抽象化を行う方法
- カプセル化と隠蔽
- OOPの礎はカプセル化だと言われてきた
- カプセル化: あるデータの操作を行なったその命令自身によってそのデータをパッケージ化する方法
- データを隠蔽する方法としても使用される
- JSではデータを隠蔽するときはクロージャを使う
- クロージャは関数の一種である
- OOPの礎はカプセル化だと言われてきた
- 動作単位としての関数
- データと動作を隠蔽することは、関数を抽象の単位にする方法のひとつにすぎない
- 抽象とは、個別の基本的な動作を保持し、あちこちで使いまわすための簡単な方法を提供すること
- 例: comparator高階関数によって、bool値を返す関数で機能を取り替えることができるようになる
- 抽象としてのデータ
- 関数型プログラミングは、ハイレベルな動作の基礎となる関数を構築することと、非常にシンプルな構造体を操作することの2点を中心に据えている
- リストデータ構造にフォーカスするなど
- データをシンプルに保ち、データを操作する抽象関数を構築することに努める
- アスリートが記録を伸ばすために食事制限するようなもの
- CSVファイルを処理するJSアプリの例
- 関数型プログラミングでは関数こそが、あるデータ型を別のデータ型に変換するための鍵となる
-
CSVファイルを処理するJSアプリの例
// CSVデータ例 // name, age, hair // Merble, 35, red // Blob, 64, blonde function lameCSV(str) { return _.reduce(str.split("\n"), function(table, row) { table.push(_.map(row.split(","), function(c) { return c.trim() })); return table; }, []) } var peopleTable = lameCSV("name, age, hair\nMerble, 35, red\nBob, 64, blonde"); peopleTable // => [["name", "age", "hair"], ["Merble", "35", "red"], ["Bob", "64", "blonde"]] // データ表現を最小限に抑えておくことで、すでにある配列処理などの関数を利用できる _.rest(peopleTable).sort(); // => [["Bob", "64", "blonde"], ["Merble", "35", "red"]] // オリジナルデータの構造はあらかじめ分かっているので、 // 適切に命名されたセレクタ関数を生成して、特定のデータにアクセスすることができる function selectNames(table) { return _.rest(_.map(table, _.first)); } function selectAges(table) { return _.rest(_.map(table, second)); } function selectHairColor(table) { return _.rest(_.map(table, function(row) { return nth(row, 2); })); } var mergeResults = _.zip; selectNames(peopleTable); // => ["Merble", "Bob"] selectAges(peopleTable); // => ["35", "64"] selectHairColor(peopleTable); // => ["red", "blonde"] mergeResults(selectNames(peopleTable), selectAges(peopleTable)); // => [["Merble", "35"], ["Bob", "64"]]
- 汎用的なコレクション処理関数を中心とした関数型のアプローチは、人間に関するデータの取り扱いに理想的
- オブジェクト指向のアプローチは人間をシミュレートする場合に最適
- オブジェクト指向のパラダイムはSimulaプログラミング言語から始まった
- オブジェクト指向のアプローチは人間をシミュレートする場合に最適
- 関数型プログラミングは、ハイレベルな動作の基礎となる関数を構築することと、非常にシンプルな構造体を操作することの2点を中心に据えている
- 関数型テイストのJavaScript
- 開発中によく利用する2つの有用な関数
-
existy
とtruthy
-
-
existy
とは何かの存在の有無を明瞭に示すための関数 - 関数型プログラミングの例
- やること
- 関数を装った「存在」の抽象を定義する
- 既存の関数を使って構築された「真偽」の抽象を定義する
- 関数を他の関数のパラメータに渡すことによって新たな動作を実現する
-
関数型プログラミングの例
function existy(x) { return x != null }; function truthy(x) { return (x !== false) && existy(x) }; { if (condition) return _.isFunction(doSomething) ? doSomething() : doSomething; else return undefined; } // ある条件がtrueの場合にのみアクションを実行する関数 function doWhen(cond, action) { if (truthy(cond)) return action(); else return undefined; } // 上記のdoWhen関数では処理できない場合にも、適用できるようにする関数 function executeIfHasField(target, name) { return doWhen(existy(target[name]), function() { var result = _.result(target, name) console.log(['結果は', result].join(' ')); return result; }); } executeIfHasField([1, 2, 3], 'reverse'); // => 結果は 3,2,1 // => [3, 2, 1] executeIfHasField({foo: 42}, 'foo'); // => 結果は 42 // => 42 executeIfHasField([1, 2, 3], 'noHere'); // => undefined [null, undefined, 1, 2, false].map(existy); // => [false, false, true, true, true] [null, undefined, 1, 2, false].map(truthy); // => [false, false, true, true, false]
- やること
- 開発中によく利用する2つの有用な関数
- 実行速度について
-
nth(array, 0)
や_.first(array)
よりも、配列のインデックスを直接指定するarray[0]
のほうが速いことは否定できない - ただ、エンジンの性能向上とベストプラクティスや最適化ツールによって関数型プログラミングで書かれたコードの実行速度を上げることはできる
- 総合的に見ると、関数型の構造を取り入れることでシステムに利益をもたらすことはできる
-
- なぜ関数型プログラミングが必要なのか
Underscoreについて
- Underscoreについて
- ときに車輪の再発明は有用だが、利用できるものがある場合は一から再実装する必要はない
まとめ
- JavaScriptは際限なく成長していく可能性を秘めている
- 関数型プログラミングの技術構成
- 抽象を導き出して、関数として構築する
- すでに存在する関数を使って、より複雑な抽象を構築する
- すでに存在する関数を別の関数に渡すことによって、さらに複雑な抽象を構築する
- 関数型プログラミングの技術構成
2. 第一級関数と作用的プログラミング
-
map, reduce, filter
という3つの関数の基本的な考え方を説明する- これらが理解の土台となる
第一級要素としての関数
- 第一級要素としての関数
-
関数型言語は第一級関数(first-class function)を容易に利用・生成可能な言語である
- JavaScriptもこれに当たる
- 静的型付け、パターンマッチング、不変性、純粋性といった特性は広くすべての言語に適用はできない
- I/Oを扱うHaskellのプログラムは命令型プログラミングの性質が非常に強く出るが、それでもHaskellが関数型言語ではないと主張する人はいない
- 第一級とは、第一級である何かがただの値であることを意味する
- JavaScriptにおいて数値は第一級の存在である
-
第一級の存在の例
// 変数に格納できる var fortytwo = function() { return 42 }; // 配列に要素として格納できる var fortytwos = [42, function() { return 42 }]; // オブジェクトのフィールドに格納できる var fortytows = { number: 42, fun: function() { return 42 } }; // 任意の時点で生成できる 42 + (function() { return 42 })(); // => 84 // 関数に引数として渡すことができる function weirdfAdd(n, f) { return n + f(); } weirdAdd(42, function() { return 42 }); // => 84 // 関数の戻り値になることができる return 42; return function() { return 42 }; // 高階関数: // 引数として関数をとる // 関数を戻り値にする _.each(['whiskey', 'tango', 'foxtrot'], function(word) { console.log(word.charAt(0).toUpperCase() + word.substr(1)); }); // => Whiskey // => Tango // => Foxtrot
-
JavaScriptにおける複数のプログラミングパラダイム
- もちろん別のパラダイムも利用できる
- 命令型プログラミング(Imperative programming)
- プロトタイプベースのオブジェクト指向プログラミング(Prototype-based OOP)
- メタプログラミング(Metaprogramming)
- 命令型プログラミング
- プログラミングの実行状態を直接操作、参照することによってプログラムを構築する
- 再利用が難しい
- プログラマーよりもコンパイラにやさしい(Sokolowski 1991)
-
99本のビールの歌詞を生成するプログラミング
// 命令型プログラミング var lyrics = []; for (var bottles = 99; bottles > 0; bottles--) { lyrics.push(bottles + "本のビールが残ってる"); lyrics.push(bottles + "本のビール"); lyrics.push("ひとつ取って、隣に回せ"); if (bottles > 1) { lyrics.push((bottles - 1) + "本のビールが残ってる"); } else { lyrics.push("もうビールは残ってない"); } } // 少しだけ関数型プログラミングのアプローチを加える // ・ドメインロジック(フレーズの生成)と、フレーズを集めて歌詞を生成する部分を分割できる // ・germanLyricsSegmentやagreementLyricsSegmentなど、異なる関数を使って異なる歌詞を生成できる function lyricSegment(n) { return _.chain([]) .push(n + "本のビールが残ってる") .push(n + "本のビール") .push("ひとつ取って、隣に回せ") .tap(function(lyrics) { if (n > 1) lyrics.push((n - 1) + "本のビールが残ってる"); else lyrics.push("もうビールは残ってない"); }) .value(); } lyricsSegment(99); function song(start, end, lyricGen) { return _.reduce(_.range(start, end, -1), function(acc,n) { return acc.concat(lyricGen(n)); }) } song(99, 0, lyricSegment);
- プログラミングの実行状態を直接操作、参照することによってプログラムを構築する
- プロトタイプベースのオブジェクト指向プログラミング
- JavaScriptでは、関数をオブジェクトのフィールド値にできる
- だが、JavaScriptの自分自身を参照する方法が関数型プログラミングとコンフリクトしてしまうこともある
-
自己参照が思わぬオブジェクトを返してしまう例
_.each; // => function(obj, iterator, context) { // ... // } /* JavaScriptの自分自身を参照する方法が、 関数型プログラミングとコンフリクトしてしまう */ var a = { name: "a", fun: function() { return this } }; a.fun(); // => { name: "a", fun: function() { return this } } var bObj = { name: "b", fun: function() { return this }}; bObj.fun(); // => { name: "b", fun: function() { return this } } var bFunc = bObj.fun; bFunc(); // => Window { ... } // => Windowオブジェクトが返ってしまう
-
- メタプログラミング
- メタプログラミングとは
- プログラミングは何かを行うためにコードを書くものだが、メタプログラミングは何かを解釈する方法を変更するためにコードを書くもの
- thisの動的な性質をメタプログラミングに活かすことができる
-
thisの参照をPoint2Dの呼び出しにバインドすることでターゲットを変更する例
function Point2D(x, y) { this._x = x; this._y = y; } new Point2D(0, 1); // => Point2D { _x: 0, _y: 1 } function Point3D(x, y, z) { Point2D.call(this, x, y); this._z = z; } new Point3D(10, -1, 100); // => Point3D { _x: 10, _y: -1, _z: 100 }
-
- メタプログラミングとは
- もちろん別のパラダイムも利用できる
-
作用的プログラミング
- 作用的プログラミング(Applicative programming)とは
- ある関数Bの引数として提供された関数Aを、関数Bを呼び出すことによって実行させるようなプログラミング
-
map, reduce, filter
という3つの作用的関数の標準的な例-
map,reduce,filterの関数内のどこかで、引数に渡された関数が実行されている(作用的プログラミング)
var nums = [1,2,3,4,5]; function doubleAll(array) { return _.map(array, function(n) { return n * 2 }); } doubleAll(nums); // => [2, 4, 6, 8, 10] function average(array) { var sum = _.reduce(array, function(a, b) { return a + b }); return sum / _.size(array); } average(nums); // => 3 /* numsから偶数のみを取得 */ function onlyEven(array) { return _.filter(array, function(n) { return (n % 2) === 0; }); } onlyEven(nums); // => [2, 4]
-
- 作用的プログラミング(Applicative programming)とは
-
コレクション中心プログラミング
- コレクションに入った多数のアイテムに同じ操作を適用するようなタスクで便利となる
-
map, identity
を使ったシンプルな例-
map, identityを使ったシンプルな例
_.map({a: 1, b: 2}, _.identity); // => [1, 2] _.map({a: 1, b: 2}, function(v,k) { return [k, v]; }); // => [['a', 1], ['b', 2]] _.map({a: 2, b: 2}, function(v,k,coll) { return [k, v, _.keys(call)]; }); // => [['a', 2, ['a', 'b']], ['b', 2, ['a', 'b']]]
-
-
作用的プログラミングのその他の例
- reduceRight
- reduceは左から右へ、reduceRightは右から左へ動作する
- 使用する関数が計算順序で結果変わる場合には有用となる
- 遅延評価にも利点があるがJavaScriptはそのような言語ではない
-
reduceRightの使用例
var nums = [100,2,25]; function div(x,y) { return x / y }; _.reduce(nums, div); // => 2 _.reduceRight(nums, div); // => 0.125 /* _.reduceRightを使って 共通関数を生成する例 */ function allOf(/* 1つ以上の関数 */) { return _.reduceRight(arguments, function(truth, f) { return truth && f(); }, true); } function anyOf(/* 1つ以上の関数 */) { return _.reduceRight(arguments, function(truth, f) { return truth || f(); }, false); } function T() { return true }; function F() { return false }; allOf(); // => true allOf(T,T); // => true allOf(T, T, T, T, F); // => false anyOf(T, T, F); // => true anyOf(F, F, F, F); // => false anyOf(); // => false
- reduceは左から右へ、reduceRightは右から左へ動作する
- find
-
findの例
_.find(['a', 'b', 3, 'd'], _.isNumber); // => 3
-
- reject
- reject関数はfilterと逆の動作を行う
- プレディケート関数がtrueを返す値を除いたコレクションを返す
-
rejectの例
_.reject(['a', 'b', 3, 'd'], _.isNumber); // => ['a', 'b', 'd'] function complement(pred) { return function() { return !pred.apply(null, _.toArray(arguments)); } } _.filter(['a', 'b', 3, 'd'], complement(_.isNumber)); // => ['a', 'b', 'd']
- reject関数はfilterと逆の動作を行う
- all
-
allの例
_.all([1, 2, 3, 4], _.isNumber); // => true
-
- any
- sortBy, groupBy, countBy
- 渡された関数の基準に応じてソートした結果を格納したコレクションを返す
-
sortBy, groupBy, countByの例
var people = [{name: "Rick", age: 30}, {name: "Jaka", age: 24}]; _.sortBy(people, function(p) { return p.age }); // => [{name: "Jaka", age: 24}, {name: "Rick", age: 30}] var albums = [ {title: "Sabbath Bloody Sabbath", genre: "Metal"}, {title: "Scientist", genre: "Dub"}, {title: "Undertow", genre: "Metal"} ]; _.groupBy(albums, function(a) { return a.genre }); // => {Metal:[title: "Sabbath Bloody Sabbath", genre: "Metal"}, // title: "Undertow", genre: "Metal"], // Dub:[title: "Scientist", genre: "Dub"]} _.countBy(albums, function(a) { return a.genre }); // => {Metal: 2, Dub: 1}
-
作用的な関数を定義してみる
- 低レベルの関数によって組み立ていき、最後の解に至る
-
function cat(/* いくつかの配列 */) { var head = _.first(arguments); if (existy(head)) return head.concat.apply(head, _.rest(arguments)); else return []; } cat([1,2,3], [4,5], [6,7,8]); // => [1, 2, 3, 4, 5, 6, 7, 8] function construct(head, tail) { return cat([head], _.toArray(tail)); } construct(42, [1,2,3]); // => [42, 1, 2, 3] function mapcat(fun, coll) { return cat.apply(null, _.map(coll, fun)); } mapcat(function(e) { return construct(3, [","]); }, [1,2,3]); // => [1, ",", 2, ",", 3, ","] function butLast(coll) { return _.toArray(coll).slice(0, -1); } function interpose(inter, coll) { return butLast(mapcat(function(e) { return construct(3, [inter]); }, coll)); } interpose(",", [1,2,3]); // => [1, ",", 2, ",", 3]
- reduceRight
-
関数型言語は第一級関数(first-class function)を容易に利用・生成可能な言語である
データ思考
- データ思考
- データ思考とは
- 無名のJavaScriptオブジェクトは、単純な連想データストアとしても使用することができる
- オブジェクトをデータマップとしてみなして操作する
-
keys, values, pluck
など
-
- データの抽象型であるテーブルへの操作となる
- SQLと似たような体験を得ることができる
- 疑似SQLのような関数定義をおこなうことができる
-
オブジェクトをテーブルとしてみなして操作する
var zombie = {name: "Bub", film: "Day of the Dead"}; _.keys(zombie); // => ["name", "film"] _.values(zombie); // => ["Bub", "Day of the Dead"] _.pluck([{title: "Chthon", author: "Anthony"}, {title: "Grendel", author: "Gardner"}, {title: "After Dark"}], 'author'); //=> ["Anthony", "Gardner", undefined] _.pairs(zombie); //=> [["name", "Bub"], ["film", "Day of the Dead"]] _.object(_.map(_.pairs(zombie), function(pair) { return [pair[0].toUpperCase(), pair[1]]; })) //=> {NAME: "Bub", FILM: "Day of the Dead"} _.invert(zombie); //=> {Bub: "name", "Day of the Dead": "film"} _.keys(_.invert({a: 138, b: 9})); // JavaScriptのオブジェクトは文字列キーのみ //=> ["138", "9"] _.pluck(_.map([{title: "Chthon", author: "Anthony"}, {title: "Grendel", author: "Gardner"}, {title: "After Dark"}], function(obj) { return _.defaults(obj, {author: "Unknown"}) }), 'author'); //=> ["Anthony", "Gardner", "Unknown"] var person = {name: "Romy", token: "j3983ij", password: "tigress"}; var info = _.omit(person, 'token', 'password'); info; //=> {name: "Romy"} var creds = _.pick(person, 'token', 'password'); creds; //=> {password: "tigress", token: "j3983ij"} var library = [{title: "SICP", isbn: "0262010771", ed: 1}, {title: "SICP", isbn: "0262510871", ed: 2}, {title: "Joy of Clojure", isbn: 1935182641, ed: 1}]; _.findWhere(library, {title: "SICP", ed: 2}); //=> {title: "SICP", isbn: "0262510871", ed: 2} _.where(library, {title: "SICP"}); //=> [{title: "SICP", isbn: "0262010771", ed: 1}, // {title: "SICP", isbn: "0262510871", ed: 2}]
- 「テーブルのような」データ
- JavaScriptオブジェクトをデータテーブルとしてみてみる
- テーブルからデータを抽出し、あるモジュールから別のモジュールにデータを引き渡すことができる
- 関数型プログラマーは、データの内容、発生するデータの変換、そして異なるレイヤーに引き渡すためのデータのフォーマットについて深く考える
- これがデータ中心思考
-
疑似SQLとデータの流れ
/* `SELECT title FROM library`の結果と対応している しかし、_.pluckを使うことで戻り値の型が異なってしまっている */ _.pluck(library, 'title'); //=> ["SICP", "SICP", "Joy of Clojure"] /* テーブルデータの形を維持する関数を作成する必要がある */ function project(table, keys) { return _.map(table, function(obj) { return _.pick.apply(null, construct(obj, keys)); }) } var editionResults = project(library, ['title', 'isbn']); editionResults; //=> [{title: "SICP", isbn: "0262010771"}, // {title: "SICP", isbn: "0262510871"}, // {title: "Joy of Clojure", isbn: 1935182641}] /* project関数はテーブルのようなデータ構造を返すため、 再度project関数を適用して、もう一段階処理することもできる */ var isbnResults = project(editionResults, ['isbn']); /* 最後に欲しいデータだけを取り出すため、 意図的にデータテーブルの構造を壊す(別のモジュールにデータを引き渡すための操作) */ _.pluck(isbnResults, 'isbn'); //=> ["0262010771", "0262510871", 1935182641]
- 抽象テーブル型に対して動作させることができる
- SQL文と同じような操作がJavaScript関数でもおこなうことができる
-
// SQL例: SELECT ed AS edition FROM library; /* as関数をJavaScriptで実装する */ function rename(obj, newNames) { return _.reduce(newNames, function(o, nu, old) { if (_.has(obj, old)) { o[nu] = obj[old]; return o; } else return o; }, _.omit.apply(null, construct(obj, _.keys(newNames)))); }; rename({a: 1, b: 2}, {'a': 'AAAA'}); //=> {AAAA: 1, b: 2} function as(table, newNames) { return _.map(table, function(obj) { return rename(obj, newNames); }) } as(library, {ed: 'edition'}); //=> [{title: "SICP", isbn: "0262010771", edition: 1}, // {title: "SICP", isbn: "0262510871", edition: 2}, // {title: "Joy of Clojure", isbn: "1935182641", edition: 1}] /* projectとasをを組み合わせて SQL文のような結果テーブルを生成できる */ project(as(library, {ed: 'edition'}), ['edition']); //=> [{edition: 1}, {edition: 2}, {edition: 1}] /* WHERE句と対応するrestrict関数を実装する */ function restrict(table, pred) { return _.reduce(table, function(newTable, obj) { if (truthy(pred(obj))) return newTable; else return _.without(newTable, obj); }, table); }; restrict(library, function(book) { return book.ed > 1; }); //=> [{title: "SICP", isbn: "0262510871", ed: 2}] restrict( project( as(library, {ed: 'edition'}), ['title', 'isbn', 'edition'] ), function(book) { return book.edition > 1; } ); //=> [{title: "SICP", isbn: "0262510871", edition: 2},] // 上記と同等のSQL文 // SELECT title, isbn, edition FROM ( // SELECT ed AS edition FROM library // ) EDS // WHERE edition > 1;
- データ思考とは
まとめ
- 本章は第一級関数に焦点をあてた
- JavaScriptが第一級関数をサポートしているということは、関数型プログラミングの実践を後押しする
-
map, reduce, filter
などで作用的プログラミングができる - オブジェクトの配列で構築したテーブル抽象型に対する、SQLに似た関係代数のセットを構築することができる
-
- 第一級関数は他のデータ型と同じように扱える
- 変数に格納できる
- 配列の要素として格納できる
- オブジェクトのフィールドに格納できる
- 必要に応じて生成できる
- 他の関数に引数として渡すことができる
- 関数の戻り値として返すことができる
- JavaScriptが第一級関数をサポートしているということは、関数型プログラミングの実践を後押しする
3. JavaScriptにおける変数のスコープとクロージャ
- JavaScriptでのコーディング一般において需要な、変数のスコープについて説明する
- 本章について
- バインディングとは
- ある値をある名前に関連づける動作のこと
- varでの変数定義、関数の引数、thisの設定、プロパティのアサインなど
- JavaScriptのthis参照に見られる動的スコープ
- 関数レベルのスコープ
- これらが最終的にクロージャの説明となる
- 生成時に関連する変数バインディングを補足する関数
- ある値をある名前に関連づける動作のこと
- JavaScriptにおいてのスコープとは
- thisバインディングの値
- thisバインディングの値によって定義される実行コンテクスト
- 変数の生存期間
- 変数値の解決の仕組み、もしくは静的スコープ
- バインディングとは
- 本章について
グローバルスコープ
- グローバルスコープ
- スコープが広がる範囲は変数の生存期間を意味する
- JavaScriptにおける一番長い生存期間を持つのがグローバルスコープ
- varキーワードを伴わない変数の宣言はグローバルスコープに生成される
- プログラム中のすべての関数やメソッドからアクセスできるスコープとなる
-
グローバルスコープの例
aGlobalVariable = 'livin la vida global'; _.map(_.range(2), function() { return aGlobalVariable }); //=> ["livin la vida global", "livin la vida global"] aGlobalVariable = 'i drink your milkshake'; aGlobalVariable; //=> "i drink your milkshake"
- グローバルスコープが嫌われる理由は、いつでもコードの変更できてしまうから(無政府状態)
- スコープが広がる範囲は変数の生存期間を意味する
- 静的スコープ(lexical scope)
- 変数とその値を参照できる範囲のこと
-
静的スコープの例
aVariable = "外"; function aFun() { var aVariable = "内"; return _.map([1,2,3], function(e) { var aVariable = "最内"; return [aVariable, e].join(' '); }); } aFun(); //=> ["最内 1", "最内 2", "最内 3"] // 一番近いスコープから始まり、バインディングを発見するまで外側に // ルックアップ対象を広げていく
動的スコープ
- 動的スコープ
- 動的スコープは単純な機構だが、一部のモダンプログラミング言語だけが主要なスコープ機構として採用している
- 動的スコープとは
- 名前と名前に紐づいている値のペアを格納したグローバルなテーブル
- どのJavaScriptエンジンの中心にも巨大なルックアップテーブルが1つ存在する
- 注意点としては、ダイナミックバインディングは明示的にアンバインドする必要がある
- 通常はプログラミング言語においてコンテクストが閉じられる際に自動的に行われる
-
動的スコープの例(履歴機能のあるReact Context APIのようなもの)
var globals = {}; function makeBindFun(resolver) { return function(k, v) { var stack = globals[k] || []; globals[k] = resolver(stack, v); return globals; }; } var stackBinder = makeBindFun(function(stack, v) { stack.push(v); return stack; }) var stackUnbinder = makeBindFun(function(stack) { stack.pop(); return stack; }) var dynamicLookup = function(k) { var slot = globals[k] || []; return _.last(slot); }; stackBinder('a', 1); stackBinder('b', 100); dynamicLookup('a'); //=> 1 globals; //=> {'a': [1], 'b': [100]} // スタックに2つ目の値をバインドする stackBinder('a', '*'); dynamicLookup('a'); //=> '*' globals; //=> {'a': [1, '*'], 'b': [100]} // 以前のバインディングに戻したい場合 stackUnbinder('a'); dynamicLookup('a'); //=> 1 // グローバルで名前付けされたスタックには問題がある function f() { return dynamicLookup('a'); }; function g() { stackBinder('a', 'g'); return f(); }; f(); //=> 1 g(); //=> 'g' globals; // {a: [1, 'g'], b: [100]}
- JavaScriptにおける動的スコープ
- これまでの説明はJavaScriptにおいて動的スコープのルールが適用されるthis参照についての議論のための準備だった
- this参照は実行コンテクストによって異なる値を指す
- 実際にはthis参照の値は呼び出す者よって決定される
- これは混乱の元となってしまう
- this参照がcallやapplyに渡されない場合や、nullにバインドされる場合には混乱は発生しない
-
this参照の値を直接操作する例
/* applyやcallを使うと this参照の値を直接操作することができてしまう */ function globalThis() { return this; } globalThis(); //=> Window { ... } globalThis.call('barnabas'); //=> 'barnabas' globalThis.apply('orsulak', []); //=> 'orsulak'
- これまでの説明はJavaScriptにおいて動的スコープのルールが適用されるthis参照についての議論のための準備だった
関数スコープ
- 関数スコープ(function scope)
- JavaScriptのvarは関数スコープであってブロックスコープではない
-
スコープやthisの挙動を理解する例
function strangeIdentity(n) { // 意図的におかしなコードを書く for(var i=0; i<n; i++); return i; } strangeIdentity(138); //=> 138 // Javaなどではiのようなforブロック内で宣言されたローカル変数に // 外部からアクセスを試みるとアクセスエラーになる。 // JavaScriptにはブロックスコープが存在しないため、アクセスできてしまう // さらに、varで宣言される変数は宣言された関数の最初まで巻き上げられて再配置される // 上記を言い換えると先ほどのコードは以下と同じことになる function strangeIdentity(n) { var i; for(i=0; i<n; i++); return i; } // this参照を使うと、簡単に関数スコープをシミュレートできる function strangerIdentity(n) { // ここでも意図的におかしなコードを書く for(this['i'] = 0; this['i'] < n; this['i']++); return this['i']; } strangerIdentity(108); //=> 108 i; //=> 108 strangerIdentity.call({}, 10000); //=> 10000 i; //=> 108 function f() { this['a'] = 200; return this['a'] + this['b']; } var globals = {'b': 2}; f.call(_.clone(globals)); //=> 202 globals; //=> {'b': 2} // グローバルコンテクストが変更されていないことを確認できる
クロージャ
- クロージャ
- クロージャとは
- それが生成された場所近辺の値を確保する関数のこと
- 自身が定義されたスコープに存在する外部のバインディングを、そのスコープの実装完了後にも使用するために確保している
- クロージャは第一級関数と密接な関係にある
- 即席でカプセル化された状態を渡すための強力な手段となる
- それが生成された場所近辺の値を確保する関数のこと
- クロージャをシミュレートする
- クロージャの一番シンプルな例は、ローカル変数を後で利用できるように確保している第一級関数
-
クロージャの例
function whatWasTheLocal() { var CAPTURED = 'あ、こんにちは'; return function() { return "ローカル変数:" + CAPTURED; }; } var reportLocal = whatWasTheLocal(); /* クロージャが変数を確保する場合、確保された変数は ある不定期間生存することができる */ reportLocal(); //=> "ローカル変数:あ、こんにちは" /* ローカル変数だけがクロージャによって確保されるわけではない 関数に渡した引数も確保される */ function createScaleFunction(FACTOR) { return function(v) { return _.map(v, function(n) { return (n * FACTOR); }); }; } var scale10 = createScaleFunction(10); scale10([1,2,3]); //=> [10, 20, 30] // FACTOR変数は実際にはcreateScaleFunctionによって返された関数が // 実行されるたびにいつでもアクセスできるように、返された関数の実行部に // 確保されている // この変数の確保がクロージャの定義そのものとなる /* thisを使用する関数スコープのシミュレータを参考にすると クロージャのシミュレートは以下のようなものになる */ function createWeirdScaleFunction(FACTOR) { return function(v) { this['FACTOR'] = FACTOR; var captures = this; return _.map(v, _.bind(function(n) { return (n * this['FACTOR']); }, captures)); }; } var scale10 = createWeirdScaleFunction(10); scale10.call({}, [5,6,7]); //=> [50, 60, 70];
-
- 自由変数
- クロージャの生成にあたって確保されるものは、自由変数と呼ばれる変数である
- 自由変数とは、もしある関数が内側に関数を持っている場合、外側の関数に定義されている変数は内側の関数から参照することができる
- 内側の関数は外側の関数からreturnで返されることで外側の関数スコープから逃げるが、この時、逃げる関数に自由変数を確保しておき、後に利用することができる
- これらの変数が自由(だれでも持ち逃げできるという意味)変数
-
自由変数の例
function makeAdder(CAPTURED) { return function(free) { return free + CAPTURED }; } var add10 = makeAdder(10); add10(32); //=> 42 /* それぞれの新しい加算関数は、それぞれの関数が生成された時点で それぞれ異なるCAPTURED変数を確保している */ var add1024 = makeAdder(1024) add1024(11); //=> 1035 add10(98); //=> 108 /* クロージャは関数も含めて、 どのようなデータ型でも確保できる */ function averageDamp(FUN) { return function(n) { return average([n, FUN(n)]); } } var averageSq = averageDamp(function(n) { return n * n }); averageSq(10); //=> 55
- シャドウイング
- xという名前の変数が関数スコープで宣言されていて、別の内側の関数スコープで、同じ名前を持った別の変数xが宣言される場合に発生する
-
シャドウイングの例
/* "値は 4320000"となる例 この場合、現在のスコープでバインディングが発見されるので、 外側で定義されている値は参照されず、シャドウイングされる */ var shadowed = 0; function varShadow() { var shadowed = 4320000; return ["値は", shadowed].join(' '); } /* 引数がない場合でも、現在地から一番近い変数バインディングとなる グローバル変数を参照する */ var shadowed = 0; function argShadow(shadowed) { return ["値は", shadowed].join(' '); } argShadow(108); //=> "値は 108" argShadow(); //=> "値は "
- クロージャの一番シンプルな例は、ローカル変数を後で利用できるように確保している第一級関数
- クロージャの使用例
-
クロージャパターンによるアクセス保護
function complement(PRED) { return function() { return !PRED.apply(null, _.toArray(arguments)); } } function isEven(n) { return (n % 2) === 0 }; var isOdd = complement(isEven); isOdd(2); //=> false isOdd(413); //=> true // isOddを定義した後にisEven関数の定義が変更された場合にどうなるか確認する function isEven(n) { return false }; isEven(10); //=> false /* クロージャに確保された変数は、 クロージャが生成された時点で確保した参照を保持し続ける */ isOdd(10); //=> true isOdd(12); //=> true /* オブジェクトoへの参照はクロージャの内部と外部の両方に存在するため 一見プライベートにみえる境界線を越えて変更が適用されてしまう */ function showObject(OBJ) { return function() { return OBJ; }; } var o = {a: 42}; var showO = showObject(o); showO(); //=> {a: 42} o.newField = 108; showO(); //=> {a: 42, newField: 108}; /* 確保された変数をプライベートデータとして扱う方法(クロージャパターンによるアクセス保護) pingpongオブジェクトは、スコープブロックとしての働きをする無名関数内に 生成されているため、PRIVATE変数は2つのクロージャを呼び出す以外には アクセスできない */ var pingpong = (function() { var PRIVATE = 0; return { inc: function(n) { return PRIVATE += n; }, dec: function(n) { return PRIVATE -= n; } }; })(); pingpong.inc(10); //=> 10 pingpong.dec(7); //=> 3 /* pingpongオブジェクトに他の関数を加えても安全 */ pingpong.div = function(n) { return PRIVATE / n }; pingpong.div(3); // ReferenceError: PRIVATE is not defined
-
- 抽象としてのクロージャ
- JavaScriptにおいてクロージャはプライベートアクセスを提供する
- 同時に、関数生成時に確保される設定情報のみを入力情報として関数を生成することで、抽象を提供できる
- makeAdderやcomplementがこの技法の例となる
- pluckerの例
-
設定情報を入れた関数を作成する例
function plucker(FIELD) { return function(obj) { return (obj && obj[FIELD]); }; } var bestNovel = {title: "Infinite Jest", author: "DFW"}; var getTitle = plucker('title'); getTitle(bestNovel); //=> "Infinite Jest" var books = [{title: "Chthon"}, {stars: 5}, {title: "Botchan"}]; var third = plucker(2); third(books); //=> "Botchan" _.filter(books, getTitle); //=> [{title: "Chthon"}, {title: "Botchan"}]
-
- JavaScriptにおいてクロージャはプライベートアクセスを提供する
- クロージャとは
まとめ
- 本章では、関数型プログラミングにとって重要かつ基礎的なトピックである変数のスコープとクロージャに焦点をあてた
- 変数のスコープについて
- グローバルスコープから始まり、静的スコープ、関数スコープを説明した
- this参照の使用方法と動作を明らかにするために動的スコープを説明した
- クロージャについて
- 既存の関数を微調整して新しい抽象を得るためにクロージャを使用する方法を紹介した
- 変数のスコープについて
- 次章では高階関数を説明する
- 他の関数に渡すことができる関数
- 関数の戻り値になることができる関数
4. 高階関数
- JavaScriptの関数が第一級の要素であることを、さらに深掘りする
- 3章では関数はデータ構造内に存在し、データとして渡すことができることを説明した
- 本章では、関数は別の関数の戻り値になることもできるということを説明する
- 高階関数(higher-order function)について
- 第一級データ型である(ただの値であるということ)
- 引数として関数をとる
- 関数を戻り値として返す
- 高階関数(higher-order function)について
引数として関数をとる関数
- 引数として関数をとる関数
- 一番顕著にその特徴が現れている関数
map, reduce, filter
- 関数を渡すことを考える:
max, finder, best
- finder関数の例
-
finder関数の例
_.max([1, 2, 3, 4, 5]); //=> 5 _.max([1, 2, 3, 4.75, 4.5]); //=> 4.75 /* maxは2つ目のパラメータを持つ高階関数 オブジェクトの配列に対しても適用できる */ var people = [{name: "Fred", age: 65}, {name: "Lucy", age: 36}]; _.max(people, function(p) { return p.age }); //=> {name: "Fred", age: 65} /* _.maxは常に比較演算子>を使って比較を行うため、 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 // MEMO: identity関数は引数をそのまま返す関数(取り替えることで異なる動作を実現する余地を残している) finder(plucker('age'), Math.max, people); //=> {name: "Fred", age: 65} finder(plucker('name'), function(x, y) { return (x.charAt(0) === "L") ? x : y }, people); //=> {name: "Lucy", age: 36}
-
- finder関数を少し引き締める
- finder関数は値を紐解く関数と比較を行う関数という2つの関数を引数にとるが、それでは過剰な場合がある
- よりシンプルな実装のbest関数を作成してみる
-
best関数を実装する
/* finder関数には柔軟性を持たせるために 比較ロジックがまったく同じになっている ・最良の値を判定する関数は、最初の引数が2つ目の引数よりも良い場合にtrueを返す ・最良の値を判定する関数は、渡された引数を比較できる形まで紐解く方法を知っている */ // finder関数内 return (bestValue === bestFun(bestValue, currentValue) ? best : current); // 「最良の値を探す」bestFun関数内 return (x.charAt(0) === "L") ? x : y; // best関数を実装する function best(fun, coll) { return _.reduce(coll, function(x, y) { return fun(x, y) ? x : y; }); } best(function(x, y) { return x > y; }, [1,2,3,4,5]); //=> 5
- finder関数は値を紐解く関数と比較を行う関数という2つの関数を引数にとるが、それでは過剰な場合がある
- finder関数の例
- 関数を渡すことをさらに考える:
repeat, repeatedly, iterateUntil
- 互いに関連する3つの関数を説明してから、これらの関数をより一般的な関数に作り直す方法を紹介する
- ある数値と値を引数に取り、その値を数値の数だけ要素として格納した配列を返す
repeat
関数-
repeat関数
function repeat(times, VALUE) { return _.map(_.range(times), function() { return VALUE }); } repeat(4, "Major"); //=> ["Major", "Major", "Major", "Major"]
-
- 値ではなく、関数を使う
- 関数を使うことで「繰り返し」の機能を拡張できる
-
/* 一般的な繰り返しの実装であれば、 よりよい方法がある */ function repeatedly(times, fun) { return _.map(_.range(times), fun); } repeatedly(3, function() { return Math.floor((Math.random()*10)+1); }); //=> [3, 7, 9] // 値に代わりに関数を取ることにより、「繰り返し」機能を拡張できる repeatedly(3, function() { return "Odelay!"; }); //=> ["Odelay!", "Odelay!", "Odelay /* DOMノードを生成する例(副作用を含む) */ repeatedly(3, function(n) { var id = 'id' + n; $('body').append($("<p>Odelay!</p>").attr('id', id)); return id; }); // Webページに3つの「<p>Odelay!</p>」がidつきで挿入される //=> ["id0", "id1", "id2"]
- 「値ではなく、関数を使え」をさらに進める
-
repeatedly
では、与えられた関数を呼ぶ回数を決めるために定数を使用している- 決まった回数を呼ぶのではなく、ある条件を満たすことで関数の実行を終了させたい場合もある
- さらに進化させた
iterateUntil
関数を作る-
iterateUntil関数
function iterateUntil(fun, check, init) { var ret = [] var result = fun(init); while (check(result)) { ret.push(result); result = fun(result); } return ret; }; iterateUntil(function(n) { return n + n; }, function(n) { return n <= 1024; }, 1); //=> [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] repeatedly(10, function(exp) { return Math.pow(2, exp+1); }); //=> [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] // 同じタスクをrepeatedlyで実行して正しい結果を得るには、反復回数を知っていけなければならない
-
-
- 一番顕著にその特徴が現れている関数
他の関数を返す関数
- 本節では関数やクロージャを返す高階関数についてより深く説明する
-
repeatedly
関数は引数を無視して決まった値を返す- 定数を返す関数は便利で、関数型プログラミングにおけるデザインパターンであると言っても差し支えない
- よくシンプルに
k
と呼ばれることもある
- よくシンプルに
-
alwaysとinvoker
repeatedly(3, function() { return "Odelay!"; }); //=> ["Odelay!", "Odelay!", "Odelay!"] // 定数を返す関数always function always(VALUE) { return function() { return VALUE; }; }; var f = always(function() {}); f() === f(); //=> true /* 新しく生成された複数のクロージャは、 それぞれが異なる値を返す */ var g = always(function() {}); f() === g(); //=> false /* alwaysを無名関数の代わりとして使用すると コードがより簡潔になる always関数は「コンビネータ」と呼ばれる */ repeatedly(3, always("Odelay!")); //=> ["Odelay!", "Odelay!", "Odelay!"] /* invokerは、メソッドを引数に取り、ターゲットとなるオブジェクトで そのメソッドを実行する関数を返す 返される関数はクロージャとなる 固定値を返すalwaysと異なり、invokerは関数実行時に与えられた値によって 特別なアクションを実行する関数を返す */ function invoker (NAME, METHOD) { return function(target /* 任意の数の引数 */) { if (!existy(target)) fial("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]]
- 定数を返す関数は便利で、関数型プログラミングにおけるデザインパターンであると言っても差し支えない
- 引数を高階関数に確保する
- 高階関数に渡す引数は返される関数の設定項目であると考える
-
makeAdderの例
/* 返される関数の動作が 引数によって設定される */ var add100 = makeAdder(100); add100(38); //=> 138
- 大義のために変数を確保する
- まずユニークな文字列を生成する関数を作ってみる
-
ユニークな文字列を生成する関数
// 単純に実装する function uniqueString(len) { return Math.random().toString(36).substr(2, len); }; uniqueString(10); //=> "3rmn4j5h9o" /* 指定された文字列を先頭に含む 文字列を生成する */ function uniqueString(prefix) { return [prefix, new Date().getTime()].join(''); } uniqueString('aregento'); //=> "aregento1358979875689" /* 要件が変更されて、 指定した文字列の末尾に連番を付与することになった */ // 期待される結果 uniqueString("ghost"); //=> "ghost0" uniqueString("turkey"); //=> "turkey1" /* 末尾の連番のための値を保持する クロージャを組み入れる */ function makeUniqueStringFunction(start) { var COUNTER = start; return function(prefix) { return [prefix, COUNTER++].join(''); } }; var uniqueString = makeUniqueStringFunction(0); uniqueString("dari"); //=> "dari0" uniqueString("dari"); //=> "dari1" /* 同じ機能をオブジェクトを使って 実現する */ var generator = { count: 0, uniqueString: function(prefix) { return [prefix, this.count++].join(''); } }; generator.uniqueString("bohr"); //=> "bohr0" generator.uniqueString("bohr"); //=> "bohr1" /* しかしこの方法は、あまり安全ではないという 弱点がある */ // countプロパティに値を代入できてしまう generator.count = "gotcha"; generator.uniqueString("bohr"); //=> "bohrNaN" // 動的にバインド generator.uniqueString.call({count: 1337}, "bohr"); //=> "bohr1337" /* 重大な詳細情報を隠蔽してアクセスさせないことが 重要である */ var omgenerator = (function(init) { var COUNTER = init; return { uniqueString: function(prefix) { return [prefix, COUNTER++].join(''); } }; })(0); omgenerator.uniqueString("lichking-"); //=> "lichking-0"
-
- 値の変異に注意
- makeUniqueStringFunction関数は、COUNTER変数に現在値を記録している
- このデータは外部からの操作に対しては安全だが、この変数が内在することで多少の混乱を招く
- 参照透過性がなくなってしまうから
- この関数が何度呼ばれたかで結果が変わってしまう
- makeUniqueStringFunction関数は、COUNTER変数に現在値を記録している
- まずユニークな文字列を生成する関数を作ってみる
- 存在しない状態に対する防御のための関数:
fnull
-
fnull
の利点- 関数の最初に長々と続いてしまう
o[k] || someDefault
のようなバリデーションパターンを避けることができる - default関数は配列要素への直接のアクセスに対して一枚レイヤーを提供する便利な関数を返す
- デフォルト値とその検証ロジックをカプセル化できる
- 関数の最初に長々と続いてしまう
-
fnull
var nums = [1,2,3,null,5]; _.reduce(nums, function(total, n) { return total * n; }); //=> 0 // nullに数値を掛けると0になってしまう doSomething({whoCares: 42, critical: null }); // 爆発 /* fnullは、nullやundefinedを デフォルトの引数に置き換える デフォルト値の代入は、必要なときがくるまで遅延される */ function fnull(fun /*, (1つ以上の)デフォルト値 */) { 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); } } var safeMult = fnull(function(total, n) { return total * n; }, 1, 1); _.reduce(nums, safeMult); //=> 30 function defaults(df) { return function(obj, key) { var val = fnull(_.identity, df[key]); return obj && val(obj[key]); }; } function doSomething(config) { var lookup = defaults({critical: 108}); return lookup(config, 'critical'); } doSomething({critical: 9}); //=> 9 doSomething({}); //=> 108
-
-
すべてを結集: オブジェクトバリデータ
- オブジェクトバリデータ
- 任意の基準にもとづいてオブジェクトの妥当性を検証する
- 外部からの命令をJSONオブジェクトとして受け取るアプリケーションの例
-
オブジェクトバリデータの例
// JSON { message: "Hi!", type: "display", from: "http://localhost:8080/node/frob", } /* いくつかのプレディケート関数(trueかfalseを返す関数)を 引数にとるchecker関数を実装する */ function checker(/* (1つ以上の)検証関数 */) { 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(); }, []); }; } /* .chain関数は次のパターンを簡潔にするために使用されている */ { errs.push(check.message); return errs; } var alwaysPasses = checker(always(true), always(true)); alwaysPasses({}); //=> [] var fails = always(false); fails.message = "人生における過ち"; var alwaysFails = checker(fails); alwaysFails({}); //=> ["人生における過ち"] /* 検証関数を生成するたびにmesasgeプロパティを 設定するのは面倒だし、自身が管理していない検証関数に プロパティを付与するのも避けたほうがよい 代わりに、検証関数を生成するためのAPIをを提供する */ function validator(message, fun) { var f = function(/* args */) { return fun.apply(fun, arguments); }; f['message'] = message; return f; } var gonnaFail = checker(validator("ZOMG!", always(false))); gonnaFail(100); //=> ["ZOMG!"] /* 検証関数を分離しておくことで それぞれにわかりやすい名称をつけることができる */ function aMap(obj) { return _.isObject(obj); } var checkCommand = checker(validator("マップデータである必要があります", aMap)); checkCommand({}); //=> true checkCommand(42); //=> ["マップデータである必要があります"] /* 設定オブジェクトが特定のキーに紐づいた値を 持っているかどうかを検証する関数を クロージャを使って作成する hasKeys関数の目的は、fun関数実行時の設定を行うこと */ function hasKeys() { var KEYS = _.toArray(arguments); var fun = function(obj) { return _.every(KEYS, function(k) { return _.has(obj, k); }); }; fun.message = cat(["これらのキーが存在する必要があります:"], KEYS).join(" "); return fun; } var checkCommand = checker( validator("マップデータである必要があります", aMap), hasKeys('msg', 'type') ); /* checkCommand関数は、与えられた引数が さまざまなチェックポイントを通って検査されるような 段階的な検証を行う組立ラインのモジュールのようなものと とらえることができる 関数型の「機械」の一方にデータを注ぎ込んて、機械の中を進むうちに 変換あるいは検証され、そして最後に「別の何か」をつくりだす 仮想組立ラインを構築するようなもの */ checkCommand({msg: "blah", type: "display"}); //=> [] checkCommand(32); //=> ["マップデータである必要があります", "これらのキーが存在する必要があります: msg type"] checkCommand({}); //=> ["マップデータである必要があります", "これらのキーが存在する必要があります: msg type"]
- 任意の基準にもとづいてオブジェクトの妥当性を検証する
まとめ
- 本章では高階関数について説明した
- 高階関数とは
- 引数として関数をとる
- 関数を戻り値として返す
-
_.max, finder, best, repeatedly, iterateUnitl
などの関数の例 - 関数の戻り値として返す関数として、
always
関数から始めた -
fnull
関数やchecker
関数などの関数を紹介した
- 高階関数とは
- 次章では、関数を用いてまったく新しい関数を合成するために、本書のここまでで学んだすべてを使う
5. 関数を組み立てる関数
- 本章では第一級関数とその使い方をさらに深く探求する
- レゴを組み合わせるように関数を合成していく方法を探る
関数合成の基礎
- 関数合成の基礎
- 関数合成とは
- 既知の方法で既存のパーツを使うことによって新たな動作を組み立て、その新たな動作も後でパーツとしても使用できるということ
- 与えられた引数によって異なる動作を行う関数を生成する
-
dispach関数
/* 関数が格納された配列を走査し、 指定したオブジェクトでそれぞれの関数を呼び出し、 最初に返された実際の値を返すdispatch関数 */ function dispatch(/* 任意の数の関数 */) { var funs = _.toArray(arguments); var size = funs.length; return function(target /*, 追加の引数 */) { 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 } } /* 配列と文字列のいずれかを渡されると、 文字列に変換して返す関数を生成する関数を作成する invoker関数とdispatch関数を同時に使うと、 if-elseでの型チェックや妥当性チェックを行う関数使わずに Array.prototype.toStringのような具体的に実装された関数の実行を行うことができる */ 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" /* dispatch関数が、 与えられた配列に格納された関数がなくなるか、 もしくは正常な値を返すまで関数を順番に実行し続けることを 活用する */ function stringReverse(s) { if (!_.isString(s)) return undefined; return s.split('').reverse().join(''); } stringReverse("abc"); //=> "cba" stringReverse(1); //=> undefined /* stringReverse関数とArray#reverseメソッドを 合成し、ポリモーフィックなpolyrev関数を生成する */ var polyrev = dispatch(invoker('reverse', Array.prototype.reverse), stringReverse); polyrev([1,2,3]); //=> [3,2,1] polyrev("abc"); //=> "cba" /* 常に正常な値を返すか、常にエラーを返す関数を デフォルトの動作を定義する「終了関数」として用意して、 他の関数と合成する */ var sillyReverse = dispatch(polyrev, always(42)); sillyReverse([1,2,3]); //=> [3,2,1] sillyReverse("abc"); //=> "cba" sillyReverse(100000); //=> 42 /* dispatch関数によって switch文による手動のディスパッチを 取り除くことができる */ 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; } performCommandHardcoded({type: 'notify', message: 'hi!'}); // notify関数が呼び出される performCommandHardcoded({type: 'join', target: 'waiting-room'}); // chageView関数が呼び出される performCommandHardcoded({type: 'wat'}); // alertが表示される /* dispatch関数を使って 同じ処理を行う関数を生成する 文字列typeと関数actionを引数にとり、 新しい関数を生成して返すisa関数を作成する isaによって返された関数は、引数objのtypeフィールドの値が、 最初に引数typeとして渡された文字列と一致する場合にのみactionを実行する それ以外の場合は、undefinedを返す undefinedが返されると、dispatch関数が次の関数を実行する合図となる (成功時にはnullとundefined以外の値を返す必要がある) */ function isa(type, action) { return function(obj) { if (type === obj.type) return action(obj); } } /* 引数として渡すオブジェクトはそのままに、 switch文での分岐と、該当なしの場合のdefault処理を dispatch関数に移譲することができる */ var performCommand = dispatch( isa('notify', function(obj) { return notify(obj.message); }), isa('join', function(obj) { return changeView(obj.target); }), function(obj) { alert(obj.type); } ); /* performCommandHardcoded関数を拡張する場合、 switch文の内部を変更する必要がある しかし、performCommand関数を拡張する場合は、 別のdispatch関数でラッピングするだけで 新たな動作を追加できる (switch文での分岐が多くなってきた際に、 処理をカテゴリ化することができるので変更容易性が高くなる) */ var performAdminCommand = dispatch( isa('kill', function(obj) { return shutdown(obj.hostname); }), performCommand, ); performAdminCommand({type: 'kill', hostname: 'localhost'}); // shutdown関数が呼び出される performAdminCommandCommand({type: 'fail'}); // alertが実行される performAdminCommand({type: 'join', target: 'foo'}); // changeView関数が呼び出される /* dispatch関数リストの前にコマンドを追加して、 オーバーライドすることで動作を制限することもできる (権限によって処理を変えることもできるため、変更容易性が高い) */ var performTrialUserCommand = dispatch( isa('join', function(obj) { alert("許可されるまで参加できません") }), performCommand, ); performTrialUserCommand({type: 'join', target: 'bar'}); // "許可されるまで参加できません"とアラートが表示される performTrialUserCommand({type: 'notify', message: 'Hi new user'}); // notify関数が呼び出される
-
- Underscoreのソースコードでよく使われているパターン
- ターゲットが存在することを確認する
- JavaScriptネイティブのメソッドの存在を確認し、存在する場合はそのメソッドを使用する
- ネイティブメソッドが存在しない場合は、動作を実装し、決められたタスクを行う
- 該当する場合、特定の型に依存したタスクを実行
- 該当する場合、特定の引数に依存したタスクを実行
- 該当する場合、引数の数に依存したタスクを実行
- map関数の例
-
map関数のソースコード
_.map = _.collect = function(obj, iterator, context) { var results = []; if (obj == null) return results; if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { results.push(iterator.call(context, value, index, list)); }); return results; }
-
- 関数合成とは
変異は低レイヤーでの操作
- 変異は低レイヤーでの操作
- 命令型スタイルで書かれた関数
- 関数型スタイルでコードを書くことは理想的
- だが、使用するライブラリの特性や、実装速度、便宜上などで命令型が適切な場合もある
- 世の中には様々な関数型のテクニックが存在し、それぞれがソフトウェア開発における複雑性を飼いならすための手段となる
-
-
`「低レイヤーと共に生きる」の原文は「Lives "Close to the Metal"」。"Close to the Metal"はAMD(ATI系)GPUのGPGPU使用をターゲットにした低レイヤーAPIのこと
-
- 関数型プログラミングを学ぶ意義
- 問題とその解決策を理解する力と、そこで使える道具の引き出しを複数持つということ
- その場その場でとるべき最善のソリューションにつながる
- 命令型スタイルで書かれた関数
カリー化
- カリー化(currying)
- すでにカリー化されたinvoker関数の例を目にしている
-
カリー化の例
/* invokerは、メソッドを引数に取り、ターゲットとなるオブジェクトで そのメソッドを実行する関数を返す 返される関数はクロージャとなる invokerは関数実行時に与えられた値によって 特別なアクションを実行する関数を返す */ function invoker (NAME, METHOD) { return function(target /* 任意の数の引数 */) { if (!existy(target)) fial("Must provide a target"); var targetMethod = target[NAME]; var args = _.rest(arguments); return doWhen((existy(targetMethod) && METHOD === targetMethod), function() { return targetMethod.apply(target, args); }); }; }; /* 少し単純な方法で実装した invoker関数 */ function rightAwayInvoker() { var args = _.toArray(arguments); var method = args.shift(); var target = args.shift(); return method.apply(target, args); } rightAwayInvoker(Array.prototype.reverse, [1,2,3]); //=> [3,2,1] /* invoker関数はカリー化されているため、 与えられたターゲットでのメソッドの実行は、 論理的な引数の数(2つ)が使い切られるまで 引き延ばされる 生成時のコンテクストにもとづいて特定の動作を行うように 「設定された」関数(クロージャ)を返すという考え方と 同じことがカリー化された関数にも当てはまる カリー化された関数は最終結果を出すために論理的に必要とされる数の パラメータをすべて埋め尽くすまで、引数を与えるたびに以前より少しだけ 「より設定された」関数を返し続ける */ invoker('reverse', Array.prototype.reverse)([1,2,3]); //=> [3,2,1]
-
- 右へカリー化するか、左へカリー化するか
- どちらを選ぶかによってAPIが少し変わる
- 本書では一番右の引数からカリー化を始めて、順次左へ進む
- JavaScriptのように任意の数の引数を渡すことができる言語では、右から左へカリー化することでオプションの引数の個数を固定できる
-
左右のカリー化の例
function leftCurryDiv(n) { return function(d) { return n/d; }; } function rightCurryDiv(d) { return function(n) { return n/d; }; } var divede10By = leftCurryDiv(10); divide10By(2); //=> 5 /* カリー化を右から行う理由の1つは、 部分結合が左から引数の固定化を行うから */ var divideBy10 = rightCurryDiv(10); divideBy10(2); //=> 0.2
- どちらを選ぶかによってAPIが少し変わる
- 自動的にパラメータをカリー化
- divide10By関数とdivideBy10関数は手動でカリー化をおこなった
- 関数の論理的パラメータの数に対応する管ずの関数を返すように明示的に記述した
- 関数を引数にとり、引数をひとつだけ取るように固定した関数を返すシンプルな高階関数が役に立つことがある
-
シンプルな高階関数curryの使用例
/* curry関数 ・関数を引数に取る ・一つだけ引数を取る関数を返す */ function curry(fun) { return function(arg) { return fun(arg); }; } parseInt('11'); //=> 11 parseInt('11', 2); //=> 3 /* parseIntを第一級関数として扱うと 2つ目のオプション引数が原因となって 混乱を招くケースがある */ ['11', '11', '11', '11'].map(parseInt); //=> [11, NaN, 3, 4] /* curry関数を使うことで parseIntが引数をひとつだけ取るよう 強制した上で実行できる */ ['11', '11', '11', '11'].map(curry(parseInt)); //=> [11, 11, 11, 11] /* 2つのパラメータをカリー化する curry2関数 */ function curry2(fun) { return function(secondArg) { return function(firstArg) { return fun(firstArg, secondArg); }; }; } /* curry2関数を使ってdivideBy10関数を 定義しなおしてみる */ function div(n, d) { return n / d}; var div10 = curry2(div)(10); div10(50); //=> 5 var parseBinaryString = curry2(parseInt)(2); parseBinaryString('111'); //=> 7 parseBinaryString('10'); //=> 2
-
- カリー化を利用して新しい関数を生成する
- クロージャがそこに確保した変数で関数の動作をカスタマイズするのと同じように、カリー化も関数のパラメータを満たすことによってカスタマイズできる
-
カリー化によって文章のような関数(virtual stentence)を作成する例
var plays = [{artist: "Burial", track: "Archangel"}, {artist: "Ben Frost", track: "Stomp"}, {artist: "Ben Frost", track: "Stomp"}, {artist: "Burial", tarck: "Archangel"}, {artist: "Emeralds", track: "Snores"}, {artist: "Burial", track: "Archangel"}, ]; _.countBy(plays, function(song) { return [song.artist, song.track].join(" - "); }); //=> { "Burial - Archangel": 3, // "Ben Frost - Stomp": 2, // "Emeralds - Snores": 1 } /* _.countByは2つ目の引数として任意の関数を取るので、 ある関数と_.countByでカリー化関数を作成することができる カリー化によってまるで文章のような表現を行う関数(virtual stentence) を作成することができる "To implementing songCount, contBy songToString"と読める */ function songToString(song) { return [song.artist, song.track].join(" - "); } var songCount = curry2(_.countBy)(songToString); songCount(plays); //=> { "Burial - Archangel": 3, // "Ben Frost - Stomp": 2, // "Emeralds - Snores": 1 }
- 3段階のカリー化でHTMLカラーコードビルダーを実装
-
3段階のカリー化でHTMLカラーコードビルダーを実装する例
function curry3(fun) { return function(last) { return function(middle) { return function(first) { return fun(first, middle, last); }; }; }; } /* _.uniq関数と合わせて使用することで、 再生された曲の一意なリストを配列で返す関数を 組み立てることができる */ var songsPlayed = curry3(_.uniq)(false)(songToString); songsPlayed(plays); //=> [{artist: "Burial", track: "Archangel"}, // {artist: "Ben Frost", track: "Stomp"}, // {artist: "Emeralds", track: "Snores"}] /* curry3を使用した場合と、_.uniqを直接呼び出す場合を 並べて比較してみる */ _.uniq(plays, false, songToString); curry3(_.uniq) (false)(songToString); /* curry3を使って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"
-
- divide10By関数とdivideBy10関数は手動でカリー化をおこなった
- 「流暢な」APIのためのカリー化
- カリー化によって流暢なAPIを作成することができる
- Haskellでは関数はデフォルトでカリー化されているが、JavaScriptではAPIをきちんと設計する必要がある
- カリー化することが適切かどうか判断する方法
- APIが高階関数を活用するかどうかを考える
- カリー化された関数を使って組み立てたchecker関数の例
-
カリー化された関数を使って組み立てたchecker関数の例
var greaterThan = curry2(function (lhs, rhs) { return lhs > rhs }); var lessThan = curry2(function (lhs, rhs) { return lhs < rhs }); var withinRange = checker( validator("10より大きい必要があります", greaterThan(10)), validator("20より小さい必要があります", lessThan(20)), ) /* これらのカリー化された2つの関数を使用すると、 大小比較を行う無名関数を直接使うよりも、 見た目がシンプルとなる */ withinRange(15); //=> [] withinRange(1); //=> ["10より大きい必要があります"] withinRange(100); //=> ["20より小さい必要があります"]
-
- カリー化によって流暢なAPIを作成することができる
- JavaScriptにおけるカリー化のデメリット
- JavaScriptは可変数の引数が許可されているため、一般的にカリー化には不利に働き、混乱を招くこともある
- カリー化は十分に注意して行う必要がある
- カリー化よりも、任意の深さまでの部分適用がより一般的に使われている
- すでにカリー化されたinvoker関数の例を目にしている
『JavaScriptで学ぶ関数型プログラミング』を読んだ 02へ続く
Discussion