🍳
『JavaScript関数型プログラミング 複雑性を抑える発想と実践法を学ぶ』を読んだ 01
JavaScriptでの関数型プログラミングの実践をさらに深掘りするために、本書を手に取りました。
本書はかなり網羅的な内容でした。
一冊目として手に取ると網羅的すぎてオエッとなってしまうかもしれませんが、ひととおり関数型プログラミングの書籍を読んでからだと、ばらばらだった知識が体系的に整理されていくのでオススメです。特にテストやイベント駆動・非同期処理への関数型のアプローチについては非常に実践的でした。
序文
- 著者が大学生や大学院生だったときは、オブジェクト指向デザインが中心だった
- 新しい言語が、ソフトウェアの課題へアプローチする方法に対して異なる視点を提供してくれるように、新しいパラダイムも同じ効果をもたらしてくれる
- JavaScriptは誰もがオブジェクト指向言語として使っているものだが、この言語は180度転換して関数型として使用することができる
- 関数型として使用することが、JavaScriptを使う上で最も強力かつ有効な方法であることがわかるのにずいぶんと時間がかかった
- その発見に気づいてほしい
本書について
本書の構成
- Part 1 発想の転換とキーコンセプト
- 関数型JavaScriptについて俯瞰する
- 第1章: 関数型プログラミングで重要な柱である純粋関数、副作用、宣言型プログラミングなどを紹介する
- 第2章: 初級・中級のJavaScript開発者に役立つ内容
- Part 2 関数型のデザインとコーディング
- 関数チェーン、カリー化、合成、モナドなどの関数型プログラミングのコアなテクニックに注目する
- 第3章: 関数チェーン、再帰やmap、filter、reduceなどの高階関数を組み合わせてプログラムを書く方法を学ぶ(Lodashを使用)
- 第4章: カリー化や合成を説明する(Ramdaなどを使用する)
- 第5章: 関数型プログラミングの理論的な領域をより深く追求して、エラー処理におけるファンクターやモナドについて説明する
- Part 3 実践テクニックのスキルアップ
- 現実の問題に取り組むために、関数型プログラミングの実務的な利点を説明する
- 第6章: 関数型プログミングに対してユニットテストやプロパティベーステストを容易に実行できることを明らかにする
- 第7章: 関数の評価に利用するJavaScriptのメモリモデルに注目し、実行時間の最適化などのテクニックを考察する
- 第8章: イベント駆動や非同期動作を扱う際の日常的な課題において、関数型プログラミングがどのように複雑性を低減するか説明する(RxJSを使用)
本書の対象読者
- オブジェクト指向の基礎知識を有し、Webアプリケーションの課題に対して一般的な認識を持つJavaScript開発者を対象にしている
- 関数型プログラミングの入門書を探していて、日頃から慣れ親しんだ構文を利用したいと考えている場合、Haskellを学習する代わりに本書を最大限に活用することができる
- 初級および中級者が、高階関数、クロージャ、カリー化、合成、ラムダ式、イテレータ、ジェネレータ、Promise、また上級者としてはモナド、リアクティブプログラミングのスキルを高めるのに役立つ
Part 1: 発想の転換とキーコンセプト
- Part 1の目標は、関数型の思考を実践して、Part 2以降で紹介する関数型のテクニックに関する基礎を築き、受け入れに向けて心の準備をすること
- 第1章では、関数型プログラミングとは何かや、心構えを紹介する
- 純粋関数、不変性、副作用、参照透過性に基づいたテクニックを紹介する
- 第2章では、関数型言語としてのJavaScriptの外観を示す
- 高階関数、クロージャ、スコープ規則などの基礎知識を得る
オブジェクト指向は可動部をカプセル化することでコードを理解しやすくする。関数型プログラミングは可動部を極力減らすことでコードを理解しやすくする。
——— Michael Feathers (Twitter)
第1章 関数型で思考する
- 本章のテーマ
- 関数型の用語で考える
- 関数型プログラミングとは何か、なぜ必要なのかを学ぶ
- 不変性と純粋関数の原則を理解する
- 関数型プログラミングのテクニックと、それらがソフトウェア設計全体に与えるインパクトを考える
- 関数型プログラミングを学ぶ意義
- 目標は開発スキルとコード全体の質を向上させること
- JavaScriptは多くの共有状態が存在する動的言語なので、開発中のコードはすぐに複雑になり、手に負えないメンテナンス性の悪いものになってしまう
- オブジェクト指向デザインは正しい方向を指し示してくれるが、それだけでは十分ではない
- データと、そのデータと相互作用する関数について注意深く考えることを促すプログラミングパラダイムが必要となる
- アプリケーションを設計する際に、次に掲げる質問を自分自身に投げかけてみる
- 拡張性
- 機能を追加するために常にコードをリファクタリングしているか?
- モジュラー化のしやすさ
- あるファイルに変更を加えたら、他のファイルにも影響するか?
- 再利用性
- コードの重複が多すぎないか?
- テスト性
- 関数のユニットテストに苦労してないか?
- 把握のしやすさ
- コードが構造化されておらず、理解しにくくないか?
- 拡張性
- 関数型プログラミングは、これらに対しての問題解決手段を提供してくれる
- 目標は開発スキルとコード全体の質を向上させること
関数型プログラミングを学ぶと役に立つのか?
- 関数型プログラミングを学ぶと役に立つのか
- 今日では、Scala、Java 8、F#、Python、JavaScriptなどのメジャーなプログラミング言語のほとんどがネイティブまたはAPIベースの関数型をサポートしている
- JavaScriptにおける関数型プログラミングの考え方
- JavaScript言語の表現力を極めて高いレベルに引き上げ、クリーンでモジュール性が高く、テスト可能で簡潔なコードの記述に役に立つ
- 関数型プログラミングはある意味、状態管理を適切に行うための言語機能を持っていないJavaScriptから、あなた自身を守るための技術とも言える
- 純粋関数をベースにしたテクニックを使うことで、増大する複雑性に直面しても簡単に把握できるコードが書けるようになる
- 関数型プログラミングはツールやフレームワークではなく、コードを記述する方法である
- オブジェクト指向出身者にとって大きなパラダイムシフトになりえる
関数型プログラミングとは何か
- 関数型プログラミングとは何か
-
関数型プログラミングとは
- 関数の利用に焦点を当てるソフトウェア開発スタイルである
- 関数型プログラミングにおいての関数とは
- 日頃関数を書いているがそれとどう違うのか?
- 副作用を避け、アプリケーションにおける状態遷移を減らすために、データに関する制御フローと処理を抽象化することを目指したもの
- HTMLページに文字列を表示するシンプルなJavaScriptプログラミングから始めてみる
-
HTMLページに文字列を表示する例
document.querySelector("#msg").innerHTML = "<h1>Hello World</h1>"; /* 上記はすべてハードコードされているため、 動的なメッセージ表示には使えない そこで、コードで関数をラッピングし、変更点をパラメータとして 渡すようにしてみる */ function printMessage(elementId, format, message) { document.querySelector( `#${elementId}` ).innerHTML = `<${format}>${message}</${format}>`; } printMessage("msg", "h1", "Hello World"); /* しかし、上記のコードはまだ完全に 再利用可能なコードとは言えない HTMLページではなくファイルにも書き込めるように してみる パラメータには単純なスカラー値ではなく、 新たな機能を持つ関数も使える 関数を他の関数と組み合わせて実行することで、 よりレベルの高い動作をさせることができる わざわざ小さな関数を集めて新たに関数を生成する 愚かな行為のように見えるが、理由がある ・1つのプログラムをより小さく分解することで、 再利用しやすく、信頼性が高く、より簡単に理解できるようになる ・プログラム全体が小さな部品の組み合わせになることで、全体把握しやすくなる */ var printMessage = run(addToDom("msg"), h1, echo); printMessage("Hello World"); /* コードをパラメータ化することで、アルゴリズムの初期条件を設定するように、 他に影響を与えず簡単に動作を変更できるコードを書くことができる */ var printMessage = run(console.log, repeat(2), h2, echo); printMessage("Get Functional");
-
- 関数型と非関数型のソリューション
- 比較すると、スタイルが根本的に異なっていることに気づく
- 両方とも同じ出力を行うが、まったく違うものに見える
- これは、関数型プログラミングが持つ宣言型の特性によるもの
- 関数型プログラミングを理解するために必要な基礎的概念
- 宣言型プログラミング
- 純粋関数
- 参照透過性
- 不変性
- 比較すると、スタイルが根本的に異なっていることに気づく
-
関数型プログラミングは宣言型である
- 宣言型プログラミングと呼ばれるカテゴリに分類される
- 宣言型プログラミングとは
- 処理がどのように実装されているか、またデータがどのように流れるかを明示することなく、一連の処理を表現するというパラダイム
- SQL文など、結果がどのように出力されるかを記述する文で構成されている一方、データを取得するための内部構造は抽象化されているもの
- 命令型・手続型とは
- 命令型プログラミングにおいてプログラムは、結果を計算するためにシステムの状態を変更しながら、上から下へ順に実行される一連の命令文として扱われる
-
命令型プログラムと関数型プログラムの対比
/* 命令型プログラミングでは、あるタスクを **どのように**実行するべきかを事細かに指示する */ var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; for (let i = 0; i < array.length; i++) { array[i] = Math.pow(array[i], 2); } array; //=> [0, 1. 4, 9, 16, 25, 36, 49, 64, 81] /* 宣言型プログラミングでは、プログラムの記述と評価を 分離する プログラムのロジックを記述した**式(expression)**を 利用する 上記と同じ問題を関数型アプローチで解決するには、(配列内の) 各要素に正しい動作を適用することと、ループ制御をシステムの 他の部分に委譲することだけに気を付ける必要がある ループのカウンタと配列のインデックスを適切に管理する責任が なくなるため、プログラムの複雑さが減少しバグの可能性を減らすことができる */ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function (num) { return Math.pow(num, 2); }); //=> [0, 1. 4, 9, 16, 25, 36, 49, 64, 81] /* ES6のアロー関数を使えば、さらに簡潔に書くことができる */ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => Math.pow(num, 2)); //=> [0, 1. 4, 9, 16, 25, 36, 49, 64, 81]
- なぜループを排除する必要があるのか
- ループは再利用が難しく、他の処理と組み合わせにくい命令型の制御構造だから
- 関数型プログラミングでは、状態の不在と不変性を可能な限り追求するため
-
純粋関数と副作用問題
- 関数型プログラミングは、純粋関数を使って不変性を持つプログラムを構築するという前提に基づいている
- 純粋関数とは
- 提供される入力値にのみ依存する
- 関数の実行中や関数呼び出しが行われる間に状態が変更する可能性があったり、隠された値や外部スコープの値には一切依存しない
- 関数自身のスコープの外にある値を一切変更しない
- グローバルオブジェクトや参照渡しされた引数を変更しない
- 提供される入力値にのみ依存する
- 命令型のデザインの要点は、変数を宣言することにある
-
不純な関数の例
/* 不純な関数の例 この関数は自身のスコープ外にある変数counterを 読み書きするから 一般的に、関数が外部のリソースを読み書きする場合、 その関数は副作用を持つと言われる */ var counter = 0; function increment() { return counter++; }
-
counter
は明示的なグローバル変更(ブラウザベースのJavaScriptの場合は、windowオブジェクト)を経由してアクセスされる -
this
でインスタンスのデータにアクセスする際にも副作用が発生する- JavaScriptにおける
this
は、関数のランタイムコンテキスト(稼働中における状態の情報や値を含むもの)によって決まるから
- JavaScriptにおける
-
- 副作用はどんな場面で発生するのか
- グローバルに存在する変数やプロパティ、データ構造を変更する
- 関数の引数の元の値を変更する
- ユーザー入力を処理する
- 同じ関数内で捕捉できなかった場合は、例外を投げる
- 画面表示やログ出力
- HTMLドキュメント、ブラウザのクッキー、データベースの問い合わせ
- 副作用を発生させないプログラムに実用性はあるのか?
- 副作用を発生させないために、オブジェクトの生成・変更やコンソール出力ができないなら、それは意味があるのか
- 実用的な関数型プログラミングはすべての状態変更を排除するわけではない
- 純粋なコードと不純なコードを分離し、状態を管理しつつ変更を最小限に抑えるためのフレームワークを提供するもの
- 副作用の問題の具体例
- 学生のデータを管理するアプリケーションを開発するチームのメンバーだとする
- 学生のレコードをSSN(ソーシャルセキュリティナンバー)で検索し、ブラウザに表示する命令型プログラムの例
- 外部のローカルオブジェクトストレージとのやり取り、IOを伴い、副作用を発生させる
-
副作用を伴う命令型のshowStudent関数
/* showStudent関数の副作用 ・この関数のシグネチャはdbをパラメータとして宣言していない ・データアクセスに外部変数(db)を使うため、呼び出しごとに変わったりnullになることもありうる ・プログラムの信頼性を損なう可能性がある ・グローバル変数elementIdは変化する可能性があり、この関数では制御できない ・グローバルなリソースであるDOMのHTML要素に直接変更を加えてしまっている ・学生の情報が見つからない場合、例外が発生して突然プログラムが終了することもありうる */ function showStudent(ssn) { let student = db.find(ssn); if (student !== null) { document.querySelector(`#${elementId}`).innerHTML = ` ${student.ssn}, ${student.firstname}, ${student.lastname} `; } else { throw new Error("Student not found!"); } } showStudent("444-44-4444");
- showStudent関数の問題点
- 外部リソースに頼っているので、柔軟性に欠け、修正やテストがしづらくなっている
- 以前に登場したprintMessageプログラムで学んだことを適用してみる
- 2つの簡単な改善を施す
- 大きな関数を複数の小さな関数に分解する
- それぞれの小さな関数はただ1つの目的を持つ
- (単一責任原則)
- 関数の処理に必要となるすべてのパラメータを明示的に定義し、副作用を減らす
- 大きな関数を複数の小さな関数に分解する
- 実際にどうやるか
- 外部ストレージやDOMのやり取りから発生する副作用を抑えることは不可能
- だが、メインのロジックから切り離すことはできる
- 管理しやすくなる
- カリー化を使う
- 関数の引数のいくつかを部分的に設定し、関数のパラメータを1つまで減らすことができる
- カリー化を適用することで、後で簡単に合成できる単項関数に変換できる
- 外部ストレージやDOMのやり取りから発生する副作用を抑えることは不可能
- showStudentを分解する
-
showStudentを分解する
const find = curry((db, id) => { let obj = db.find(id); if (obj === null) { throw new Error("Object not found!"); } return obj; }); const csv = (student) => `${student.ssn}, ${student.firstname}, ${student.lastname}`; const append = curry((selector, info) => { document.querySelector(selector).innerHTML = info; }); var showStudent = run(append("#student-info"), csv, find(db)); showStudent("444-44-4444");
- 以前と比べてほんの少しだけ改善でき、利点が増えた
- 3つの再利用可能なコンポーネントに分割され、より再利用性を高めることができた
- 関数の再利用によって管理が必要なコード量を抑えることができた
- 宣言型スタイルで書くことにより、このプログラムで行われる処理のステップが明確になり可読性が高まった
- HTMLオブジェクトとのやり取りを専用の関数に移したことにより、不純な処理から純粋な処理を分離できた
- 改善の余地はあるが、副作用を抑えたことで外部要因の変化に対しての耐久性が少しだけ増した
-
- 2つの簡単な改善を施す
-
参照透過性と代替性
- 参照透過性とは、関数が純粋である場合にみられる性質のこと
- 純粋性とは関数の引数とその戻り値の間に純粋な関連性があるということを意味している
- ある関数が同じ入力に対して常に同じ結果を返すということ
- 関数が参照透過性を持つとどんないいことがあるか
- テストがしやすくなるだけでなく
- より簡単にプログラム全体を把握することができるようになる
- 数学的推論に近づいていく
- 関数が副作用を持っていると、等式推論は適用できなくなる
- どうやって副作用を抑制するか
- 関数のすべてのパラメータをスカラー値のようにあらかじめ定義しておくことで、ほとんどの副作用は抑制できる
- しかし、オブジェクトが参照渡しされる場合は、その中身を変更しないように注意する必要がある
-
averageの例
/* 参照透過性を持たない関数の例 */ var counter = 0; function increment() { return ++counter; } /* 外部の変数への依存を取り除き、 外部変数を関数の明示的なパラメータに変更することで、 関数が参照透過性を持つようにする 新しく書き直した関数は、入力値が同じあれば 常に同じ結果を返す */ var increment = (counter) => counter + 1; /* 純粋関数で構築されたプログラムは、 全体が把握しやすく頭の中でシステムの状態のモデルを 構築できる そのため、書き換えや代替によって修正しやすくなる Program = [Input] + [func1, func2, func3, ...] -> Output sumとsize関数が参照透過性を持つ場合、引数を簡単に書き換えられる */ var input = [80, 90, 100]; var average = (arr) => divide(sum(arr), size(arr)); average(input); //=> 90 var average = divide(270, 3); //=> 90 /* 参照透過性を持つ関数を組み合わせることで、 数学的推論をするようにプログラムの振る舞いを理解できるようになる */ var input = [80, 90, 100]; var sum = (total, current) => total + current; var total = (arr) => arr.reduce(sum); var size = (arr) => arr.length; var divide = (a, b) => a / b; var average = (arr) => divide(total(arr), size(arr)); average(input); //=> 90
- 参照透過性とは、関数が純粋である場合にみられる性質のこと
-
データの不変性を維持
- 不変データとは、一度生成されると変更できないデータのこと
- JavaScriptにおいての不変なデータ
- JavaScriptのすべてのプリミティブ値(StringやNumberなど)は本質的に不変である
- しかし、配列などのオブジェクトは不変性を持っていない
- これは副作用の原因となる
-
sortDesc関数の例
/* 一見副作用が存在しないように見える関数でも、 副作用を持つことがある Array.prototype.sort関数は状態を持っており、 メソッドを呼び出す配列自身の要素をソートする */ var sortDesc = (arr) => arr.sort((a, b) => b - a); var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; sortDesc(arr); //=> [9, 8, 7, 6, 5, 4, 3, 2, 1]
- 関数型プログラミングの定義
-
関数型プログラミングとは、純粋関数を宣言的に評価することである。純粋関数は、外部から観測可能な副作用を回避することで、不変性を持つプログラムを生成する。
- 常に純粋な処理を行うようにして、データを絶対に変異させないパッケージ化された処理単位として関数を認識することで、多くのバグの可能性をつぶすことができる
- 関数型プログラミングのコアな原則への理解は、複雑性を克服する正しい道に導いてくれる
-
- よくあるJavaScript開発者が直面する問題
- 外部の共有変数に頼り切った巨大な関数や条件分岐の多用、そして明確な構造が存在しないこと
- 管理されておらずデバッグが難しい多数の実行ファイル、蜘蛛の巣のようなグローバルデータの網が理解を阻むこと
- これが多くのJavaScriptアプリケーションの現在の姿である
-
関数型プログラミングとは
関数型プログラミングの利点
- 関数型プログラミングの利点
-
本節の目標
- 関数型への気づきに導くこと
- 問題を単純な関数の組み合わせとして直感的に認識できるようにする
- 紹介するトピックは今後の各章への導入にもなる
- 関数型プログラミングがJavaScriptアプリケーションにもたらす利点を、3点にフォーカスして探る
- タスクをシンプルな関数に分解する
- 円滑なチェーンを使ってデータを処理する
- リアクティブパラダイムを使ってイベント駆動コードの複雑さを低減する
- 関数型への気づきに導くこと
-
タスクをシンプルな関数に分解する
- 関数型プログラミングの実質
- 関数型プログラミングの実質とは、分解(プログラムを小さい処理単位に分ける)と合成(小さい処理単位を結合する)の相互作用と言える
- これが関数型プログラムをモジュール化し、効率を高める秘密となる
- この小さい処理単位こそが、関数である
- 関数型プログラミングの実質とは、分解(プログラムを小さい処理単位に分ける)と合成(小さい処理単位を結合する)の相互作用と言える
- まずは、特定のタスクを論理的なサブタスク(関数)に分解することから始める
- showStudent関数を分解すると、find, csv, appendとなる
- 関数は単一の機能を持つべき
- 単一責任の原則と密接な関係がある
- showStudent関数を分解すると、find, csv, appendとなる
- 関数を組み合わせるためには、関数の入力と出力の型を一致させる必要がある
- 純粋性と参照透過性が支援してくれる
- 関数の複雑さはそのパラメータの数と直接関係することがある
- run関数は、関数型プログラミングの最も重要なテクニックの1つである合成を実現する関数である
- 前の関数の出力を後の関数の入力値として処理を行う
-
f・g = f(g(x))
: fとgの合成 - 2つの関数が合成可能となる条件は、パラメータの数と型が揃っていること
-
- run関数の代わりに、まったく同じ関数合成の機能を持つcompose関数を正式名称として使ってみる
-
compose関数の例
var showStudent = compose(append("#student-info"), csv, find(db)); showStudent("444-44-4444");
- 他の関数を引数にとる関数を高階関数と呼ぶ
-
- 前の関数の出力を後の関数の入力値として処理を行う
- 関数合成のメリット
- 独立したパーツそれぞれが持つ機能を組み合わせつつ、式全体を理解しやすいコードにしてくれる
- これは他のプログラミングパラダイムではなかなか実現できないこと
- 抽象のレベルを引き上げ、詳細な実装を表に出すことなくすべてのステップの概要を明確にすることができる
- 独立したパーツそれぞれが持つ機能を組み合わせつつ、式全体を理解しやすいコードにしてくれる
- 純粋性と参照透過性が支援してくれる
- 関数型プログラミングの実質
-
円滑なチェーンを使ってデータを処理する
- チェーンとは関数を連続して呼び出すもの
- それらの関数は共通のオブジェクト形式の戻り値を返す
- これによりとてもシンプルなコードが書けるようになる
- チェーンによってできること
- 命令型で書いたコードから排除できるもの
- 変数宣言
- 変数の値の変更
- ループ
- if-else文
- なぜ排除するのか
- 命令型の制御フロー(ループや分岐など)は実行時の条件によって異なる実行パスを通るため、関数の複雑性を増加させてしまうから
- 複雑性が高まることによって、テストが困難になるため
-
2つ以上の授業を受けている学生の成績の平均値を計算するプログラムの例
/* 命令型アプローチの例 3つのステップ ・対象となる学生(2つ以上の授業を受けている)を選択する ・対象学生の成績を抽出する ・対象学生全員の成績の平均値を計算する */ var totalGrades = 0; var totalStudentsFound = 0; for (let i = 0; i < enrollment.length; i++) { let student = enrollment[i]; if (student !== null) { if (student.enrolled > 1) { totalGrades += student.grade; totalStudentsFound++; } } } var average = totalGrades / totalStudentsFound; //=> 90 /* Lodashによって関数チェーンでつなげる例 関数チェーンは必要になって初めて実行される(遅延評価)ため、 使わない部分を含めてコード全体を実行することを避け、 CPUリソースを節約できる 他の関数型言語が持つ必要呼び(call-by-need)という動作に似ている */ _.chain(enrollment) .filter((student) => student.enrolled > 1) .pluck("grade") .average() .value(); //=> 90
- 命令型で書いたコードから排除できるもの
- エラー処理はどうするのか
- 例外を投げることは副作用の1つである
- 机上の関数型プログラミングには例外は存在しないが、現実的には避けて通ることはできない
- 関数型デザインパターンでは、可能な限り純粋なエラー処理を実装し、本当に例外的条件でのみ例外を投げるようにする
- チェーンとは関数を連続して呼び出すもの
-
非同期アプリケーションの複雑性に対処する
- 今までは、モジュール化され、テストが容易でかつ拡張性が高いアプリケーションを構築するのに、関数型プログラミングがどのように役立つかを見てきた
- では、ユーザー入力やWebリクエスト、ファイルシステム、データベースなどとのやり取りで発生する、非同期やイベントベースで取得されるデータを扱う場合に、どこまで役立つものなのか?
- コールバックパターンは、成功とエラー処理のロジックが複雑に入れ子状に記述されるためコードのフローを破壊して読みにくくなる
- これを改善する
- リアクティブプログラミングは、おそらく関数型プログラミングの最もエキサイティングで興味深い適用法の1つ
- このパラダイムによって、クライアントでもサーバでも取り扱っている非同期コードやイベント駆動コードの複雑性を劇的に減らすことができる
- コードの抽象度を上げることでビジネスロジックに集中できる
- 学生のSSNを読み出してその値を検証する例
-
学生のSSNを読み出してその値を検証する例
/* 一般的な命令型のアプローチの例 簡単な処理だが、複雑さの一端が垣間見える すべてのビジネスロジックを一箇所に集めるため、 モジュラー製を欠いている この関数は外部変数に依存しているため、再利用ができない ・副作用: 関数スコープの外部のデータを使う ・データを変更する ・入れ子の分岐ロジック */ var valid = false; var elem = document.querySelector("#student-info"); elem.onkeyup = function (event) { var val = elem.value; if (val !== null && val.length !== 0) { val = val.replace(/^\s*|\s*$|\-s/g, ""); if (val.length === 9) { console.log(`Valid SSN: ${val}!`); valid = true; } } else { console.log(`Invalid SSN: ${val}!`); } }; /* リアクティブプログラミングの例(RxJSを使用) mapやreduceやアロー関数(ラムダ式)を使うため、 このコードは関数型プログラミングの特徴を持っている このプログラミングパラダイムはオブザーバブル(observable: 観測可能) によって実現される observableを使ってデータのストリームをsubscribe(購読)でき、 関数合成やチェーン化を行なった関数でそのストリームを処理できる すべての処理は完全に不変であり、すべてのビジネスロジックは個別の関数に 分離されている */ RegExp.Observable.fromEvent(document.querySelector("#student-ssn"), "keyup") .pluck("srcElement", "value") .map((ssn) => ssn.replace(/^\s*|\s*$|\-/g, "")) .filter((ssn) => ssn !== null && ssn.length === 9) .subscribe((validSsn) => { console.log(`Valid SSN ${validSsn}`); });
-
- 関数型プログラミングは、リアクティブな形式で行わなければならないわけではない
- だか、関数型で考えるようになると、リアクティブプログラミングにおいて、半ば強制的に、関数型プログラミングを使わざるをえなくなる
- 関数型リアクティブプログラミング(FRP)のアーキテクチャとなる
- 関数型プログラミングはパラダイムシフトである
- どのようなプログラミングの課題に対してもアプローチが劇的に変わる
- 関数型は、オブジェクト指向デザインと二者択一の選択ではなく、併用されるものである
- マルチスレッドプログラミングをより単純にできる
- 普遍性と状態共有を厳密に制御するため
- JavaScriptはシングルスレッドのプラットフォームなので、本書では範囲外ではある
- 次章では、関数型とオブジェクト指向のデザインの主要な差異を取り上げる
- 今までは、モジュール化され、テストが容易でかつ拡張性が高いアプリケーションを構築するのに、関数型プログラミングがどのように役立つかを見てきた
-
本節の目標
まとめ
- まとめ
- 純粋関数を使うコードはグローバル状態に対して変更や破壊をしない
- コードのテストや保守が簡単になる
- 関数型プログラミングは、把握しやすい宣言型コードで行われる
- 関数合成とアロー関数(ラムダ式)でコード全体の可読性を高め、コード量を減らすことができる
- コレクションのデータ処理は、mapやreduceなどの操作をつなげた関数チェーンによって円滑に実行できる
- 関数型プログラミングは、関数をビルディングブロック(ロジックや機能ごとに分類し、まとめてブロック化したもの)のように扱う
- 第一級関数や高階関数を使って、コードのモジュール性や再利用性を高めることができる
- リアクティブプログラミングと関数型プログラミングを使って、イベントベースのプログラムの複雑性を低減できる
- 純粋関数を使うコードはグローバル状態に対して変更や破壊をしない
第2章 関数型言語としてのJavaScript
自然言語に支配的なパラダイムが存在しないように、JavaScriptにも支配的なパラダイムは存在しない。開発者は多くのアプローチ(手続型、関数型、オブジェクト指向)から洗濯して、必要に応じて組み合わせることができるのだ。
——— Angus Croll 『If Hemingway Wrote JavaScript』
- 本章のテーマ
- なぜJavaScriptが関数型言語として適切なのか
- JavaScriptはマルチパラダイム開発を可能にする言語
- 不変性と変更ポリシー
- 高階関数と第一級関数を理解する
- クロージャとスコープのコンセプトを探る
- クロージャの活用例
- アプリケーションを開発するときには、適切なプログラミングモデルを使うことが重要となる
- 第1章では関数型プログラミングに注目すべき理由を説明した
- しかし、パラダイムはただのプログラミングモデルにすぎない
- 適切な言語を使うことによって初めて活用できる
- 本章ではオブジェクト指向と関数型プログラミングを混合したハイブリッド言語であるJavaScriptのクイックツアーを行う
- JavaScriptが関数型に利用できる理由と、利用する上で不足している点を説明する
- 不変性サポートの不足、高階関数とクロージャなど
- JavaScriptが関数型に利用できる理由と、利用する上で不足している点を説明する
なぜJavaScriptなのか
- なぜJavaScriptなのか
-
「なぜJavaScriptなのか?」という疑問に答える
- 第1章では「なぜ関数型なのか?」という疑問に答えた
- 答えはシンプルで、JavaScripがあらゆる場所で使われているから
- この世に出たあらゆる言語のうち最も広範に利用されている言語の1つ
- Webの言語として他の類を見ない普及率を誇っている
- その意味では、2位以下を大きく引き離して利用されている関数型プログラミング言語と言える
- JavaScriptについて
- C言語に似た文法を持っているにもかかわらず、LispやSchemeといった関数型言語からも強く影響を受けている
- 高階関数、クロージャ、配列リテラル、アロー関数、定数、イテレータ、Promiseなどが使える
- 実際にJavaScriptにおける主要な処理単位は関数となっている
- アプリケーションの動作を規定するだけでなく、オブジェクトの定義、モジュールの作成、イベント処理などにも利用される
- JavaScriptはオブジェクト指向的であるのと同時に関数型でもあることを理解することが重要である
- 実際に関数型言語として使われることはほとんどない
- 変異を伴う処理や手続型の制御構造、オブジェクトインスタンスの状態変化を利用されることが多い
- 関数型スタイルを採用するとこれらの制御構造や状態変化は事実上排除される
- オブジェクト指向と関数型のパラダイムの相違点への理解が明確になれば、関数型の世界によりスムーズに飛び込むことができる
-
「なぜJavaScriptなのか?」という疑問に答える
関数型プログラミングvs.オブジェクト指向プログラミング
- 関数型プログラミングvs.オブジェクト指向プログラミング
-
関数型プログラミングとオブジェクト指向プログラミングの違いとは
- 両方とも中大規模システム開発に利用できる
- ScalaやF#などのハイブリッド言語は、1つの言語に両方のパラダイムを融合している
- JavaScriptも似た機能を備えているので、両方のパラダイムを有効活用することがJavaScriptマスターへの鍵となる
- どのように組み合わせて使うかは、個人的な好みや解決すべき問題に左右される
- よって、両者の違いと重なる部分を理解しておくことは、それぞれの利用シーンでの使い分けや、考察する視点を決めるのに役立つ
-
Student
オブジェクトを中心としたシンプルな学習管理システムのモデルを考えてみる- クラスや型の階層の点から考えると、自然と
Student
はPerson
のような「人」を表す型のサブタイプとなる- さらに、
Student
から派生させたCollegeStudent
には、このオブジェクトに特化した機能性を追加できる - オブジェクト指向プログラムは本質的に新たな派生オブジェクトをコード再利用の主な手法として利用する
-
CollegeStudent
は親となる型のデータと振る舞いを再利用する
-
- さらに、
- クラスや型の階層の点から考えるときの問題点
- 既存オブジェクトへ機能を追加すると、そのオブジェクトの子孫(継承先)でその機能を必要としない場合、状況を複雑にしてしまう可能性がある
-
firstname
とlastname
はPerson
とそのすべての子孫に必要だが、workAddress
はStudent
オブジェクトではなく、たとえばEmployee
オブジェクト(Person
から派生)のようなものにとって必要な要素である
-
- 既存オブジェクトへ機能を追加すると、そのオブジェクトの子孫(継承先)でその機能を必要としない場合、状況を複雑にしてしまう可能性がある
- クラスや型の階層の点から考えると、自然と
- オブジェクト指向と関数型では、アプリケーションに おいてデータ(オブジェクトのプロパティ)と振る舞い(関数)の整理方法が異なる
- オブジェクト指向アプリケーションでは
- 一般的にそのほとんどが命令型で記述される
- 直接または継承された変異可能な状態について、整合性を維持するため、オブジェクトベースのカプセル化に強く依存している
- インスタンスメソッドを通じてその状態の露出や操作を行う
- この結果、オブジェクトのデータと、そのデータを処理するための振る舞いに強い結合が生じてしまう
- 結合性の高いパッケージが形成されることになる
- このようなパッケージを形成することは、オブジェクト指向プログラムの目標である
- また、中心となる抽象化の形がオブジェクトであることの理由にもなっている
- 結合性の高いパッケージが形成されることになる
- オブジェクト指向型のコードでは、より多くのデータ型を処理できるように、きめ細かくインスタンスメソッドを定義する
- 関数型プログラミングでは
- 呼び出し元からデータを隠す必然性を排除し、とてもシンプルな型の小さいデータで動作する
- すべての要素が不変であるため、オブジェクトを自由に直接操作できる
- 操作にはオブジェクトのスコープ外にある、一般化された関数を利用する
- これは、データは動作とゆるく結合しているということになる
- 関数型のコードでは、多くのデータ型で共通に使えるよう、関数の動作を粗く定義する
- このパラダイムでは、関数こそが抽象化の形となる
- オブジェクト指向アプリケーションでは
- 機能とデータ型の図
- 図の右上に行くほど2つのパラダイムの違いが大きくなる
- 著者が見た最も素晴らしいオブジェクト指向のコードは、両方のパラダイムをちょうど交点で有効に活用していた
- オブジェクトは不変のエンティティ(1単位として扱われるデータのまとまり)または値として扱い、機能はオブジェクトに作用する関数として分離されていた
- 著者が見た最も素晴らしいオブジェクト指向のコードは、両方のパラダイムをちょうど交点で有効に活用していた
- これらの原則に従うと
Person
メソッドはどうなるか- オブジェクト指向プログラミングと関数型のアプローチの違い
- オブジェクト指向プログラミングは継承階層(Personの子としてのStudentなど)の定義に焦点を当て、メソッドとデータが強く結合する
-
fullname()
はPerson
から派生したすべてのオブジェクトで動作する
-
- 関数型プログラミングでは、さまざまなオブジェクト(データ型)に対して利用可能なポリモーフィック関数を優先し、thisの利用を極力避ける
- 複数の型に対して共通のロジックを持つ関数を定義できる
-
fullname()
をスタンドアロンの関数に分離することで、オブジェクト内のデータへアクセスする際にthis
の利用を避けることができる-
this
は、メソッドスコープ外にあるインスタンスレベルのデータにアクセスできてしまうため、副作用を起こすことがあるから
-
- オブジェクトデータは特定のコードと密接に関連付けられることがないので、再利用性とメンテナンス性に優れている
-
原則に従ってPersonメソッドを改良する
/* メソッドではthisを使って オブジェクトの状態にアクセスする */ get fullname() { return [this._firstname, this._lastname].join(' '); } /* thisを利用する代わりに、 引数として渡されたオブジェクトを利用する */ var fullname = (person) => [person.firstname, person.lastname].join(" "); fullname(person); //=> "Alonzo Church"
- オブジェクト指向プログラミングと関数型のアプローチの違い
- 関数型プログラミングでは、オブジェクト指向プログラミングのように多くの派生型を定義する代わりに、関数を引数として渡すことで関数の動作を拡張することができる
- オブジェクト指向プログラミングと関数型プログラミングの根本的な違い
- 異なるデータ型に新たな動作を与える場合、オブジェクト指向プログラミングでは主に継承を行い、関数型プログラミングでは関数合成を行う
- オブジェクト指向のデザインはデータの特性とデータの関係性に焦点を当て、関数型プログラミングは実行する処理、つまり振る舞いに焦点を当てる
- 両方のプログラミングパラダイムを利用してアプリケーションを構築するアプローチは強力になる
- オブジェクト指向ではモデルを構成する型同士が自然な関係を持つ、クラスベースの表現豊かなドメインモデルが手に入る
- 関数型では、これらの型を処理できる純粋関数を持つことができる
- これらのパラダイムをどのように組み合わせるからは開発者の判断次第である
-
Person
クラスと、Person
クラスを拡張したStudent
クラスで構成されたモデルの例-
オブジェクト指向プログラミングと関数型プログラミングのパラダイムの違いの例
class Person { constructor(firstname, lastname, ssn) { this._firstname = firstname; this._lastname = lastname; this._ssn = ssn; this._address = null; this._birthYear = null; } get ssn() { return this._ssn; } get firstname() { return this._firstname; } get lastname() { return this._lastname; } get address() { return this._address; } get birthYear() { return this._birthYear; } set birthYear(year) { this._birthYear = year; } set address(addr) { this._address = addr; } toString() { return `Person(${this._firstname}, ${this._lastname})`; } /* 与えられたPersonオブジェクトと同じ国に住む すべてのPersonを探す 与えられたStudentと同じ国に住み同じ学校に通う 他のStudentを探す */ peopleInSameCountry(friends) { var result = []; for (let idx in friends) { var friend = friends[idx]; if (this._address.country === friend.address.country) { result.push(friend); } } return result; } } class Student extends Person { constructor(firstname, lastname, ssn, school) { super(firstname, lastname, ssn); this._school = school; } get school() { return this._school; } studentsInSameCountryAndSchool(friends) { // superを利用して親クラスからデータを要求する var closeFriends = super.peopleInSameCountry(friends); var result = []; for (let idx in closeFriends) { var friend = closeFriends[idx]; if (friend.school === this.school) { result.push(friend); } } return result; } } var curry = new Student("Haskell", "Curry", "111-11-1111", "Penn State"); curry.address = new Address("US"); var turing = new Student("Alan", "Turing", "222-22-2222", "Princeton"); turing.address = new Address("England"); var church = new Student("Alonzo", "Church", "333-33-3333", "Princeton"); church.address = new Address("US"); var kleene = new Student("Stephen", "Kleene", "444-44-4444", "Princeton"); kleene.address = new Address("US"); church.studentsInSameCountryAndSchool([curry, turing, kleene]); //=> [kleene] /* 関数型プログラミングでは、状態から動作を分離しておくことで、 各データ型で動作する新たな関数を定義し、それらの関数を 組み合わせてより多くの処理をサポートすることができる これにより、データ型を格納するシンプルなオブジェクトと、 これらのオブジェクトを引数にとる機能性の高い関数が手に入る さらに関数合成によってより特化した機能を実現できる */ function selector(country, school) { return function (student) { return student.address.country() === country && student.school === school; }; } var findStudentsBy = function (friends, selector) { return friends.filter(selector); }; /* findStudentsBy関数は、Personとそのすべての派生クラスのオブジェクト、 そして任意の学校と国の組み合わせを入力値として受け付ける */ findStudentsBy([curry, turing, church, kleene], selector("US", "Princeton")); //=> [church, kleene]
-
- オブジェクト指向プログラミングと関数型プログラミングの根本的な違い
- 両方とも中大規模システム開発に利用できる
-
JavaScriptオブジェクトの状態管理
- プログラムの状態とは
- ある時点でオブジェクトの保持されたすべてのデータのスナップショットと定義できる
- しかしながらJavaScriptはオブジェクトの状態管理においては最悪といえる言語である
- オブジェクトプロパティは動的で、いかなるときも変更、追加、削除が可能である
-
Person
オブジェクトの_address
はカプセル化できない- クラスの外からでも好きなようにアクセスでき、削除まで可能
- プロパティを動的に生成するなどのテクニックもあるが、中規模以上になると保守がとても難しいコードになってしまう
- 自由には大きな責任が伴う
- 副作用のない純粋関数を使うと、コードの保守は把握がより簡単になる
- 関数に適用する純粋性の原則の論理をシンプルなオブジェクトにも適用できる
- JavaScriptにおいてのオブジェクト状態の管理は、この言語を関数型言語として利用にするにあたっての最重要課題となる
- 関数に適用する純粋性の原則の論理をシンプルなオブジェクトにも適用できる
- プログラムの状態とは
-
オブジェクトを値として扱う
- プリミティブなデータ型は本質的に不変である
- 文字列や数値など
- このように振る舞う型を、関数型プログラミングでは値と呼ぶ
- 不変性を実現するには、すべてのオブジェクトを実質的に値として扱わなければならない
- そうすることで、オブジェクトの値が変更される心配をせずに関数を使えるようになる
- JavaScriptのオブジェクトは任意の時点で属性の追加・削除・変更が可能な単なる入れ物である
- どうすれば不変にすることができるのか?
- JavaScriptのプリミティブ型は変更できない
- その値を参照する変数の状態は変更できる
- データへの不変の参照を提供、または最低でも擬似的に実現しなければならない
- JavaScriptのプリミティブ型は変更できない
- 実践的なJavaScriptでの関数型プログラミング
-
const
で定数の参照を宣言する- 定数は再代入や再宣言はできない
-
constを利用する
const gravity_ms = 9.806; gravity_ms = 20; //=> Uncaught TypeError: Assignment to constant variable.
- しかしこれだけでは、関数型プログラミングが必要とするレベルでは変異の問題を解決できない
-
オブジェクトの内部状態の変更する例
/* 次のコードはエラーを発生させず、正常に動作してしまう */ const student = new Student("Alonzo", "Church", "666-66-6666", "Princeton"); student.lastname = "Mourning";
-
- オブジェクトの内部状態の変更を防ぐ必要がある
- 値オブジェクト(Value Object)パターンを採用する
- 値オブジェクトとは
- 値オブジェクトは、その同一性を検証する際、名称や参照ではなく、その値に対してのみ検証する
- 値オブジェクトは、一度宣言されるとその状態は変更できない
- 値オブジェクトの例
- pair(一対のもの)、point(位置)、zipCode(郵便番号、 coordinate(座標)、money(金額)、date(日付)などがある
-
値オブジェクトとしてのzipCodeの実装例
/* zipCode関数の例 郵便番号の内部状態に対する関数を使用して、 アクセスを保護することができる オブジェクトリテラルのインターフェースを返して、 呼び出し元に公開するメソッドを一部に限定しつつ、 _codeや_locationを擬似的なプライベート変数として 扱うことによって上記を実現している 擬似的なプライベート変数は、クロージャを介してのみ アクセスできるようになっている */ function zipCode(code, location) { let _code = code; let _location = location; return { code: function () { return _code; }, location: function () { return _location; }, fromString: function (str) { let parts = str.split("-"); return zipCode(parts[0], parts[1]); }, toString: function () { return _code + "-" + _location; }, }; } const princetonZip = zipCode("08544", "3345"); princetonZip.toString(); //=> "08544-3345"
- この関数に返されるオブジェクトは、実質的に変異メソッドを持たないプリミティブ値のように振る舞う
- toStringメソッドは純粋関数ではないが、純粋関数のように振る舞い、純粋な文字列表現を提供する
- 値オブジェクトとは
- 値オブジェクトは軽量であり、関数型でもオブジェクト指向でも簡単に扱える
-
const
とともに利用することで、文字列や数値に近い特性を持つオブジェクトを生成できる -
coordinateの例
function coordinate(lat, long) { let _lat = lat; let _long = long; return { latitude: function () { return _lat; }, longitude: function () { return _long; }, translate: function (dx, dy) { return coordinate(_lat + dx, _long + dy); }, toString: function () { return "(" + _lat + "," + _long + ")"; }, }; } const greenwich = coordinate(51.4778, 0.0015); greenwich.toString(); //=> '(51.4778,0.0015)' greenwich.translate(10, 10).toString(); //=> '(61.4778,10.0015)'
-
- 値オブジェクトは、関数型プログラミングに影響を受けたオブジェクト指向デザインパターンである
- 異なるパラダイムがお互いを見事に補完する一例
- ただ、理想的なパターンだが実世界に存在するすべての問題をモデル化するには十分ではない
- 実際は、階層データ(PersonやStudentなど)を扱う場合や、既存のオブジェクトをそのまま利用する場合などは別の仕組みが必要になる
- そのために、
Object.freeze
関数が用意されている
- そのために、
- 実際は、階層データ(PersonやStudentなど)を扱う場合や、既存のオブジェクトをそのまま利用する場合などは別の仕組みが必要になる
- 値オブジェクト(Value Object)パターンを採用する
- プリミティブなデータ型は本質的に不変である
-
可動部分をディープフリーズ(再帰的にフリーズ)
- JavaScriptは、プロパティの不変性を可能にする内部的なメカニズムをサポートしている
- writable(上書き可能)などの隠しオブジェクトのメタプロパティを制御することによって実現できる
- falseに設定すると、
Object.freeze
関数はオブジェクトの状態変更を防止できる
- falseに設定すると、
-
Object.freezeの例
var person = Object.freeze(new Person("Haskell", "Curry", "444-44-4444")); person.firstname = "Bob"; //=> Uncaught TypeError: Cannot assign to read only property 'firstname' of object '#<Object>' /* Object.freezeは 継承されたプロパティも不変にできる ただ、入れ子状のオブジェクトプロパティは 不変にできないため、注意が必要 */ class Address { constructor(country, state, city, zip, street) { this._country = country; this._state = state; this._city = city; this._zip = zip; this._street = street; } get street() { return this._street; } get city() { return this._city; } get state() { return this._city; } get zip() { return this._zip; } get country() { return this._country; } } var person = new Person("Haskell", "Curry", "444-44-4444"); person.address = new Address( "US", "NJ", "Princeton", zipCode("08544", "1234"), "Alexander St." ); person = Object.freeze(person); person.address._country = "France"; //=> エラー発生せず person.address.country; //=> "France" /* Object.freezeは浅いフリーズ処理である 完全にフリーズさせるには、オブジェクトの入れ子構造を たどって手動で再帰的にフリーズ(deep freeze)させる必要がある */ var isObject = (val) => val && typeof val === "object"; function deepFreeze(obj) { if (isObject(obj) && !Object.isFrozen(obj)) { Object.keys(obj).forEach((name) => { deepFreeze(obj[name]); }); Object.freeze(obj); } return obj; }
- writable(上書き可能)などの隠しオブジェクトのメタプロパティを制御することによって実現できる
- まだこれだけでは足りない
- JavaScriptの複雑性と困難さを軽減するには、新たなオブジェクトをオリジナルから生成する際の厳格なポリシーが役にたつ
- レンズ(Lenses)と呼ばれる関数型アプローチを紹介する
- これは、オブジェクトの変更を普遍的に一括管理する最良の選択肢となる
- JavaScriptの複雑性と困難さを軽減するには、新たなオブジェクトをオリジナルから生成する際の厳格なポリシーが役にたつ
- JavaScriptは、プロパティの不変性を可能にする内部的なメカニズムをサポートしている
-
レンズを使ってオブジェクトグラフを操作
- オブジェクト指向プログラミングの不変性の問題
- オブジェクト指向プログラミングでは、メソッドを呼び出して、状態を保つオブジェクトの内部のコンテンツを変更することは、当然だと考えられている
- この方式では、オブジェクトの状態が正しく取得されることが保証できない
- オブジェクトの不変性を前提とする一部のシステムの機能を破壊してしまう可能性がある
- コピーオンライト(copy-on-write: 書き込み時にコピーすること)戦略で、各メソッドを呼び出しから新たなオブジェクトを返す実装もできるが、冗長になるためエラーの温床になりかねない
- ドメインモデル内の全ての型のすべてのプロパティにセッター関数を実装することになってしまう
-
その他すべてのプロパティの状態についても複製する必要がある例(Personクラス)
... set lastname(lastname) { return new Person(this._firstname, lastname, this._ssn); }
- オブジェクト指向プログラミングでは、メソッドを呼び出して、状態を保つオブジェクトの内部のコンテンツを変更することは、当然だと考えられている
- 必要なのは、状態を保つオブジェクトを不変的に変更する方法である
- (純粋な)関数参照とも呼ばれるレンズを使う
- 状態を持つデータ型の属性を不変的にアクセスし操作できる関数型プログラミングのソリューション
- レンズの内部的な動作はコピーオンライトに似ている
- 状態の管理と複製を適切に処理できる内部ストレージコンポーネントを利用する
- Ramdaを使った例
-
Ramdaのレンズを使用する
/* R.lensPropsを使って Personのlastnameプロパティを レンズでラッピングする */ var person = new Person("Alonzo", "Church", "444-44-4444"); const lastnameLens = R.lensProp("lastname"); R.view(lastnameLens, person); // get lastname()ゲッターメソッドと類似している //=> "Church" /* R.setを呼び出すと、新たな値を持ったオブジェクトの 新たなコピーを返し、元のインスタンス状態を保持する (コピーオンライトを実現している) 既存のオブジェクトや、自分の管理範囲外のオブジェクトでも レンズを使って新たなオブジェクトを生成できる */ var newPerson = R.set(lastnameLens, "Mourning", person); newPerson.lastname; //=> "Mourning" person.lastname; //=> "Church" /* レンズはプロパティの入れ子にも対応している Personのaddressプロパティを例とする */ person.address = new Address( "US", "NJ", "Princeton", zipCode("08544", "1234"), "Alexander St." ); var zipPath = ["address", "zip"]; var zipLens = R.lensPath(R.path(zipPath), R.assocPath(zipPath)); R.view(zipLens, person); //=> zipCode("08544", "1234") /* レンズは不変セッターを実装しているため、 入れ子のオブジェクトを変更して、 新たなオブジェクトを返すことができる */ var newPerson = R.set(zipLens, zipCode("90210", "5678"), person); var newZip = R.view(zipLens, newPerson); //=> zipCode('90210', '5678') var originalZip = R.view(zipLens, person); //=> zipCode('08544', '1234') newZip.toString() !== originalZip.toString(); //=> true
-
- これで関数型で使えるゲッターとセッターを手に入れることができた
- レンズは防護用の不変的なラッパーを提供するだけでなく、フィールドへのアクセスのロジックをオブジェクトから分離してくれる
- thisへの依存を取り除き、どのようなオブジェクトの内容でも参照し操作できる強力な関数を提供する
- 関数型の根本原理と相性がとてもいい
- レンズは防護用の不変的なラッパーを提供するだけでなく、フィールドへのアクセスのロジックをオブジェクトから分離してくれる
- オブジェクトを適切に処理する方法がわかった
- ここからは関数型プログラミングの核心部となる、アプリケーションの可動部のエンジンとなる関数を見ていく
- (純粋な)関数参照とも呼ばれるレンズを使う
- オブジェクト指向プログラミングの不変性の問題
-
関数型プログラミングとオブジェクト指向プログラミングの違いとは
関数
- 関数
-
関数とは
- 関数は、関数型プログラミングにおいて基本となる作業単位(unit of work)である
- すべてがこの関数を軸にして展開される
- 関数について
-
()
演算子によって評価される、呼び出し可能な式である - 関数型プログラミングは数学と極めて似た動作をするので、関数は有効な結果(nullやundefined以外)を生成する場合にのみ存在意義がある
- そうでなければ、副作用で外部データを変更してしまうと考えられる
- 式(値を生成する関数)と文(値を生成しない関数)を分けて考える
- 式と文
- 命令型や手続型プログラミングは通常、順番に連続した文で構成される
- 関数型プログラミングは完全に式で構成される
-
- JavaScriptの関数は、関数型スタイルの土台である2つの重要な特徴を備えている
- 関数が第一級であること
- 高階であること
- 関数は、関数型プログラミングにおいて基本となる作業単位(unit of work)である
-
第一級オブジェクトとしての関数
- JavaScriptにおける第一級とは、プログラミング言語における関数を第一級オブジェクトとも言われる実際のオブジェクトにすること
-
JavaScriptでの関数の例
function multiplier(a, b) { return a * b; } /* 無名関数やラムダ式としても 変数に代入できる */ var square = function (x) { return x * x; }; var square = (x) => x * x; /* メソッドとしてオブジェクトの プロパティに代入できる */ var obj = { method: function (x) { return x * x; }, }; square; // function (x) { // return x * x; // } /* 関数は関数コンストラクタによる生成も 可能である(ただし、あまり推奨されない) これはJavaScriptで関数が第一級オブジェクトであることを 証明している */ var multiplier = new Function("a", "b", "return a * b;"); multiplier(2, 3); //=> 6
-
- JavaScriptの関数はすべてFunction型のインスタンスである
- Function型のインスタンスとは
- 関数の
length
プロパティからは、設定されたパラメータの数を取得できる -
apply()
やcall()
などのメソッドに対してパラメータなどのコンテキストを指定することで、関数を呼び出すことができる - 無名関数の右辺は
name
プロパティが空の関数オブジェクトである
- 関数の
- Function型のインスタンスとは
-
sort()
などのJavaScript関数は、代入可能(assignable)であるだけでなく、他の関数を引数にとることができる高階関数である-
sort()の例
/* 関数の引数として無名関数を渡すことで、 関数の動作や拡張や特化を行うことができる Array.sort(comparator)の例 */ var fruit = ["Coconut", "apples"]; fruit.sort(); //=> ['Coconut', 'apples'] // Unicodeでは大文字が小文字よりも前にソートされる var ages = [1, 10, 21, 2]; ages.sort(); //=> [1, 10, 2, 21] // 数値は文字列として変換され、その文字列のコードポイント(1つ1つの文字に割り当てられた数値)で // ソートされるため、10は2よりも前に来てしまう /* 適切な数値比較関数を引数に渡して、 peopleオブジェクトのリストを年齢(数値比較)でソートする */ people.sort((p1, p2) => p1.getAge() - p2.getAge());
-
- JavaScriptにおける第一級とは、プログラミング言語における関数を第一級オブジェクトとも言われる実際のオブジェクトにすること
-
高階関数
- 関数は通常のオブジェクトのように振る舞うので、関数を関数の引数として渡したり、関数の戻り値として返したりすることができる
- これを高階関数と呼ぶ
-
高階関数
/* 関数を他の関数に渡すことができる */ function applyOperation(a, b, opt) { return opt(a, b); } var multiplier = (a, b) => a * b; applyOperation(2, 3, multiplier); //=> 6 /* 関数を戻り値として返すことができる */ function add(a) { return function (b) { return a + b; }; } add(3)(3); //=> 6
- JavaScriptの関数は第一級オブジェクトであり、高階関数でもあるので、値のように振る舞うことができる
- つまり関数とは、与えられる入力値に基づき不変的に定義された、「まだ実行されていない値」である
- これは関数型プログラミングでの原則である
- 関数チェーンでこの原則が顕著に表れる
- 関数チェーン
- 関数チェーンを組み立てる際、常に関数名を利用する
- 関数名はプログラムの一構成部分を示すものであり、式全体のうちの一部として実行される
- 高階関数を組み合わせて、小さなパーツから意味のある式を作ることで、プログラムをシンプルに記述できる
- 米国(US)在住者リストを出力する例
-
命令型コードから関数型コードへの高階関数を活用したリファクタリング例
/* 命令型コードとしての 最初のアプローチ */ function printPeopleInTheUs(people) { for (let i = 0; i < people.length; i++) { var thisPerson = people[i]; if (thisPerson.address.country === "US") { console.log(thisPerson); } } } printPeopleInTheUs(people); /* 米国以外の在住者リストも出力するという 要件が追加された場合 高階関数を使って、対象の各thisPersonに 適用される動作を抽象化できる */ function printPeople(people, action) { for (let i = 0; i < people.length; i++) { action(people[i]); } } var action = function (person) { if (person.address.country === "US") { console.log(person); } }; printPeople(people, action); /* printPeopleが高階関数の利点を十分に 活用できるようにリファクタリングする 高階関数を使うと宣言的パターンが見えてくる これにより、プログラムの目的を明確に表すことができる 可読性が向上する 最初の命令型アプローチよりも柔軟性を高めることができた ・簡単に選択基準や出力先を変更(または設定)できる ・テストやデバッグが容易になる ・関数を組み合わせて新たな関数を作ることができる */ function printPeople(people, selector, printer) { people.forEach(function (person) { if (selector(person)) { printer(person); } }); } var inUs = (person) => person.address.country === "US"; printPeople(people, inUs, console.log); /* さらに関数型プログラミングを深掘りしてみる */ var countryPath = ["address", "country"]; var countryL = R.lens(R.path(countryPath), R.assocPath(countryPath)); var inCountry = R.curry((country, person) => R.equals(R.view(countryL, person), country) ); people.filter(inCountry("US")).map(console.log);
-
- 米国(US)在住者リストを出力する例
- 関数は通常のオブジェクトのように振る舞うので、関数を関数の引数として渡したり、関数の戻り値として返したりすることができる
-
関数呼び出しの方法
- JavaScriptの関数呼び出しのメカニズムは、他の言語と少し違う
- JavaScriptでは、関数が呼び出されるランタイムコンテキスト(つまり、関数本体内のthisの値)をプログラマーが完全に制御できる
- JavaScriptの関数はいくつか異なる方法で呼び出せる
-
関数の呼び出し方の例
/* グローバル関数として呼び出す thisへの参照は、グローバルオブジェクト、 またはundefined(strictモード)に設定される */ function doWork() { this.myYear = "Some value"; } // doWorkを呼ぶとthisの値はグローパルオブジェクトを指すようになる doWork(); /* メソッドとして呼び出す thisへの参照は、メソッドのオーナーに設定される これはJavaScriptが持つオブジェクト指向の重要な特性である */ var obj = { prop: "Some property", getProp: function () { return this.prop; }, }; // オブジェクトのメソッドを呼ぶと、thisの値はそのオブジェクトを指す obj.getProp(); /* コンストラクタとしてnewを使って呼び出す 新たに生成されるオブジェクトへの参照が 暗黙的に返される */ function MyType(arg) { this.prop = arg; } // newを使って関数を呼ぶと、thisへの参照は新たに生成され、暗黙的に返されるオブジェクトとなる var someVal = new MyType("some argument");
-
- 他のプログラミング言語とは異なり、thisの参照は関数がどのように利用されるか(グローバルに、オブジェクトメソッドとして、コンストラクタとしてなど)によって決まる
- コード内で定義されている位置は関係しない
- この仕様は、関数が実行されるコンテキストに常に注意しなければならず、コードが理解しにくくなる可能性がある
- よって、関数型プログラムにおいてはthisはほぼ使用されない
- むしろ是が非でも避けるべきとされる
- thisは、ライブラリやツールの機能がコンテキストの変更を必要とするような特殊なケースよく利用される
- そのような場合には、しばしば関数メソッドapplyやcallも使われる
- JavaScriptの関数呼び出しのメカニズムは、他の言語と少し違う
-
関数メソッド
- JavaScriptでは、関数のプロトタイプに定義されているメタ関数のような、callやapplyで関数を呼び出すことができる
-
negate関数の例
/* 与えた関数の計算結果の否定を返す関数を 生成するnegate関数を実装する */ function negate(func) { return function () { return !func.apply(null, arguments); }; } function isNull(val) { return val === null; } var isNotNull = negate(isNull); isNotNull(null); //=> false isNotNull([]); //=> true /* applyは配列を引数にとるが、 callはリストを引数にとる thisArgがnullであれば、関数コンテキストは グローバルオブジェクトになり、関数は通常の グローバル関数のように動作する メソッドがstrictモード内の関数の場合は、thisArgは nullが設定される しかしながら、関数型プログラミングではコンテキストの状態に依存しないため、 thisArgは利用されない */ Function.prototype.apply(thisArg, [argsArray]); Function.prototype.call(thisArg, arg1, arg2, ...);
-
- JavaScriptでは、関数のプロトタイプに定義されているメタ関数のような、callやapplyで関数を呼び出すことができる
-
関数とは
クロージャとスコープ
- クロージャとスコープ
-
クロージャについて
- JavaScript登場以前は、クロージャはいくつかの特殊なアプリケーションに使われる関数型言語にしか備わっていなかった
- メインストリームの開発言語として初めてJavaScriptがこの考え方を採用した
- これはコードの書き方を大きく変化させた
- zipCode関数の例でクロージャを説明する
-
zipCodeによるクロージャの例
function zipCode(code, location) { let _code = code; let _location = location; return { code: function () { return _code; }, location: function () { return _location; }, }; } /* zipCode関数が返すオブジェクトリテラルは、 そのスコープ外で定義されている変数にアクセスできる つまり、zipCodeの実行結果によって返されるオブジェクトは、 この関数の実行後であっても関数内で宣言された情報にアクセスできる */ const princetonZip = zipCode("08544", "3345"); princetonZip.code(); //=> "08544"
-
- クロージャの活用方法
- プライベート変数をエミュレートする
- サーバからデータを取得する
- ブロックスコープの変数を強制する
- クロージャとは
- 関数をその宣言された時点の環境にバインド(束縛)するデータ構造のこと
- 関数定義を含む静的スコープ、または動的スコープとも呼ばれる
- 関数にその周囲へのアクセスを許すため、コードを簡潔にでき可読性を高めることができる
- クロージャは関数のスコープを継承することであり、オブジェクトのメソッドがその継承されたインスタンス変数へアクセスできることに似ている
- クロージャとスコープは両方とも親を参照している
-
クロージャの動き
/* 本質的に、入れ子の関数addとraiseは 計算本体だけではなく、関連するすべての変数を 内包するスナップショットをパッケージしていると 考えることができる */ function makeAddFunction(amount) { function add(number) { return number + amount; } return add; } function makeExponentialFunction(base) { function raise(exponent) { return Math.pow(base, exponent); } return raise; } var addTenTo = makeAddFunction(10); addTenTo(10); //=> 20 var raiseThreeTo = makeExponentialFunction(3); raiseThreeTo(2); //=> 9 /* クロージャの動作の様子 ローカル変数(innerVar)は、関数makeInner実行後に ガベージコレクションなどで消滅するので、undefinedが 出力されるのではないかと普通は思う 実は、裏でクロージャが動作していてこの出力を可能にしている この関数が宣言された時点でのすべての変数を記憶しており、 それらの変数が無効になることを防いでいる グローバルスコープもクロージャの一部となり、outerVarへのアクセスも 可能となる */ var outerVar = "Outer"; function makeInner(params) { var innerVar = "Inner"; function inner() { console.log(`I can see: ${outerVar}, ${innerVar}, ${params}`); } return inner; } var inner = makeInner("Params"); inner(); //=> I can see: Outer, Inner, Params
- 関数をその宣言された時点の環境にバインド(束縛)するデータ構造のこと
- JavaScript登場以前は、クロージャはいくつかの特殊なアプリケーションに使われる関数型言語にしか備わっていなかった
-
グローバルスコープの問題
- グローバルスコープとは
- スクリプトの一番外側で宣言された、どの関数にも含まれないすべてのオブジェクトや変数はグローバルスコープの一部である
- どこからでもアクセス可能
- 関数型プログラミングの目標は、観察可能な変更が関数から波及することを極力抑えること
- グローバルで共有されるデータを使用すると、常にすべての変数の状態を把握しておく必要が生じてしまう
- プログラムが複雑になる
- グローバル変数の読み書きの際に外部依存関係が発生するので、関数内に副作用をもたらす
- 問題を引き起こす可能性が高くなる
- グローバルで共有されるデータを使用すると、常にすべての変数の状態を把握しておく必要が生じてしまう
- グローバル変数の使用は避ける必要がある
- スクリプトの一番外側で宣言された、どの関数にも含まれないすべてのオブジェクトや変数はグローバルスコープの一部である
- グローバルスコープとは
-
JavaScriptの関数スコープ
- 関数スコープとは
- JavaScriptの推奨するスコープメカニズム
- 関数内で宣言されたすべての変数はその関数のローカル変数となり、関数の外からは参照できない
- また、関数実行後にローカル変数は削除される
-
関数スコープの例
function doWork() { let student = new Student(...); let address = new Address(...); // 処理が続く }
- 変数の名前解決はプロトタイプの名前解決チェーンに似ている
- まず自身のスコープ内で変数を探し、見つからない場合はスコープを外側にさかのぼって探していく
- JavaScriptのスコープのメカニズム
- 変数の関数スコープ(ローカル)を探す
- ローカルスコープで変数が見つからない場合は構文上1つ外側のスコープを探し、見つからない場合はグローバルスコープまでさかのぼる
- グローバルスコープでも見つからない場合、undefinedが返る
-
JavaScriptのスコープのメカニズム
var x = "Some value"; function parentFunction() { function innerFunction() { console.log(x); } return innerFunction; } var inner = parentFunction(); inner(); //=> 'Some value'
- JavaScriptの推奨するスコープメカニズム
- 関数スコープとは
-
擬似的なブロックスコープ
- ES5では、ブロックスコープをサポートしない
- ブロックスコープとは
for
やwhile
、if
、switch
などの文の{}
に含まれるスコープのことを言う - ES6では
let
が導入されたことで、スコープの曖昧さを排除できるようになった -
varとletのスコープの差
if (someCondition) { var myVar = 10; } myVar; //=> 10 // ES5には関数スコープしか存在しないため、 // ブロックスコープで宣言された変数はスコープのどこであっても // アクセスできてしまう /* 上記の問題を克服する方法が いくつかある */ function doWork() { if (!myVar) { var myVar = 10; } console.log(myVar); //=> 10 } doWork(); //=> 10 // 内部的なJavaScriptのメカニズムでは、 // 変数と関数の宣言は現行のスコープのトップレベル、 // つまり、関数のスコープへと巻き上げられる /* 上記の特性により、ループの安全性が 損なわれる */ var arr = [1, 2, 3, 4]; function processArr() { function multipleBy10(val) { i = 10; return val * i; } for (var i = 0; i < arr.length; i++) { arr[i] = multipleBy10(arr[i]); } return arr; } processArr(); //=> [10, 2, 3, 4] // ループのカウンタiの宣言は、関数processArrの先頭に // 暗黙的に巻き上げられ、関数multipleBy10のクロージャの // 一部となってしまう // キーワードvarを指定せずにiを使用すると、iはmultipleBy10の // ローカル変数にはならず、意図せずにループカウンタのiを10に // 上書きしてしまう // 巻き上げられたループカウンタ宣言iにはundefinedが設定され、 // ループが実行される時点で0が代入される /* ES6で追加されたletキーワードを使うことで、 ループカウンタを、正しくバインドできる */ for (let i = 0; i < arr.length; i++) { arr[i] = multipleBy10(arr[i]); } i; //=> Uncaught ReferenceError: i is not defined
- ブロックスコープとは
- ES5では、ブロックスコープをサポートしない
-
クロージャの実践的な利用方法
- クロージャの利用方法
- プライベート変数のエミュレーション
- 非同期のサーバーサイド呼び出し
- 擬似的なブロックスコープ変数
- プライベート変数のエミュレーション
- JavaScriptにはprivate変数を指定するキーワードや、オブジェクトのスコープの範囲でアクセスする機能を用意していない
- クロージャを使うとプライベート変数をエミュレートできる
-
zipCode, coordinate
など - 返ってくるオブジェクトリテラルのメソッドを使えば、その関数のローカル変数にアクセスできるが、そのローカル変数自体には直接アクセスできない
- 実質的にプライベート変数となる
-
- クロージャは、グローバルでのデータの共用を回避するための名前空間の管理にもなる
- モジュールのプライベートメソッドやデータ全体を隠蔽することによってクロージャを活用できる
- これはモジュールパターンと呼ばれる
- 即時関数(IIFE: immediately invoked function expression)を使って内部変数をカプセル化し、必要な機能だけを外部に開放することで、グローバルな参照の数を大きく減らすことができる
- モジュールの骨格の例
-
モジュールパターン
var MyModule = (function MyModule(export) { let _myPrivateVar = ...; export.method1 = function () { // 処理 }; export.method2 = function () { // 処理 }; return export; }(MyModule || {}));
- MyModuleの仕組み
- MyModuleはグローバル空間で生成され、functionキーワードで作成される関数式に渡される
- スクリプトがロードされると即時に実行される
- ローカル変数
_myPrivateVar
は関数内でのみアクセスできる - 外部に露出された2つのメソッドが含まれるクロージャを通して、モジュールの内部プロパティに安全にアクセスできる
-
- このモジュールパターンは、多くの関数型ライブラリに採用されている
- 非同期のサーバーサイド呼び出しを行う
- JavaScriptの関数はコールバックとして、他の関数に渡すことができる
- 第一級かつ高階な関数なため
- コールバックをイベント処理のフックとして使用する
- サーバーにリクエストを送信して、データを受信した時点で通知をもらうなど
-
コールバックの例
/* getJSONの例 getJSONは、成功時に実行する関数とエラー時に実行する関数の 2つのコールバックを引数にとる高階関数である */ getJSON( "/students", (students) => { getJSON( "/students/grades", (grades) => processGrades(grades), (error) => consol.log(error.message) ); }, (error) => console.log(error.message) );
- JavaScriptの関数はコールバックとして、他の関数に渡すことができる
- ブロックスコープ変数をエミュレートする
- JavaScriptにはブロックスコープが存在しないという根本的な問題へ対処するために、ブロックスコープを生成する
-
let
を使うことでも解決できるが、関数型アプローチでは少し異なる角度から攻める - クロージャとJavaScriptの関数スコープ、そして
forEach
を使う
-
- ループ処理本体をループ内部に実質的にラッピングして、コレクションに対してイテレーションを実行する際に非同期処理を呼び出す
-
forEachの例
arr.forEach(function (elem, i) { // 処理 });
-
- JavaScriptにはブロックスコープが存在しないという根本的な問題へ対処するために、ブロックスコープを生成する
- クロージャの利用方法
-
クロージャについて
まとめ
- まとめ
- JavaScriptはオブジェクト指向と関数型プログラミングの特徴を色濃くもつ万能言語である
- オブジェクト指向に不変性の概念を取り入れると、関数型プログラミングとうまく共存できる
- 高階関数・第一級関数といったJavaScriptの関数の性質は、関数型で書く際の基礎的要素である
- クロージャには多数の実践的な活用方法がある
- 情報の隠蔽、モジュール開発、複数データ型を扱う粒度の粗い関数に対してパラメータ化された振る舞いを引き渡すなど
『JavaScript関数型プログラミング 複雑性を抑える発想と実践法を学ぶ』を読んだ 02 へ続く
Discussion