JavaScript関数型プログラミング Ch.4 モジュール化によるコードの再利用
- まえがき
- メソッドチェーンと関数パイプライン
- 互換性のある関数のための要件
- カリー化された関数評価
- 部分適用とパラメータ束縛
- 関数パイプラインを合成する
- 関数コンビネータを使ってフロー制御を管理する
- まとめ
まえがき
- 前章: 単一のラッパーオブジェクト(
_.chain(collection)
とか)のメソッドチェーン- メソッドなのでオブジェクト依存
- 強く結合
- 限られた表現力
- 本章: パイプライン
- 関数合成でより緩く結合
- 柔軟性が高い
メソッドチェーンと関数パイプライン
- Haskellスタイルの関数型宣言の話
<function-name> :: <Inputs*> -> <Output>
例) 文字列を引数にとり、真偽値を返す関数
isEmpty :: String -> Boolean
メソッドをまとめてチェーンにする
Lodashを使ったコード
_.chain(names) .filter(isValid) .map(s => s.replace(/_/, ' ')) .uniq() .map(_.startCase) .sort() .value();
- 命令型コードよりも可読性が高い
- が、不自然
- 所有するオブジェクト(
_.chain(name)
)に強く依存- チェーンに適用できるメソッドの種類が制限される
- 他ライブラリの関数や自作関数を簡単には接続できない
関数をパイプライン状に配置する
- メソッドではない、単なる関数をつなげる
- ライブラリが提供するものでもユーザ定義でも
- 緩い結合
- 関数の入出力に互換が必要
- アリティ(引数の個数)
- 引数の型
- 関数の入出力に互換が必要
互換性のある関数のための要件
compose(f, g)
- type
f
の返却値の型とg
の引数の型とは一致しなければならない
- arity (lengthとも)
g
はf
の返却値を受け取るために、少なくとも1つの引数が必要
型互換の関数
- JSは緩く型付けされるから大きな問題じゃない
- 【補】TypeScriptとかだともっといいですね
- Haskellスタイル等で型宣言コメントを書いておくとわかりやすいコードになる
関数とアリティ:タプルの場合
- 純粋関数は、引数によってのみ結果が決まる(参照透過性)
- したがって、引数が多い = 複雑
- 単一引数・単一戻り値が最も単純
- 実用上、複数戻り値を返したいことがある
- 複数の戻り値・引数をまとめて1つにしたい
isValid :: String -> (Boolean, String)
- 何がまずかったのかエラーメッセージを添えたい
- tuple
-
(Boolean, String)
こういうやつ - 異種型混合の不変オブジェクト
- 残念ながらJSネイティブサポートはない
- p.118のようなソースコードで実装可能
-
これは良くない:
return { status: false, message: 'Input is too long', };
- 一時的な型の作成
- データをグループ化するためだけに新しい型を定義している
- モデルが必要以上に複雑化
- 可変
これも良くない:
return [false, 'Input is too long'];
- 異種混合の配列
- 「配列」は、同じ型のオブジェクトを保存するもの
- そうしないと型チェックコードまみれになる
- 可変
カリー化された関数評価
- アリティを減らす別の方法
f :: (a, b, c) -> d curry(f) :: ((a, b, c) -> d) -> a -> b -> c -> d
- 入力
(a, b, c)
を、個々の単一引数の呼び出しに分解curry(f)は
、a
を受け取る関数を返すcurry(f)(a)
は、b
を受け取る関数を返すcurry(f)(a)(b)
は、c
を受け取る関数を返すcurry(f)(a)(b)(c)
は、d
を返す
- Ramda実習
関数インタフェースをエミュレートする
- Factory Method Pattern
- OOPならでは?いいえ
// fetchStudentFromDb :: DB -> (String -> Student) const fetchStudentFromDb = R.curry(function (db, ssn) { return find(db, ssn); }); // fetchStudentFromArray :: Array -> (String -> Student) const fetchStudentFromArray = R.curry(function (darr, ssn) { return arr[ssn]; }); // findStudent :: String -> Student const findStudent = userDb ? fetchStudentFronDb(db) : fetchStudentFromArray(arr); // Student findStudent('444-44-4444');
- findStudent利用側は、実装を意識しない
- DB実装
- Array実装(キャッシュか何かか)
再利用可能な関数テンプレートを実装する
- Log4jsライブラリ
console.log
より優れている- 設定可能項目が多く、逐一指定するとコード重複が起きてしまう
- そこでカリー化ですよ
// logger :: (String, String, String, String, String) -> * // R.curry(logger) :: String -> String -> String -> String -> String -> * // log :: String -> String -> * const log = R.curry(logger)('alert', 'json', 'FJS'); log(''ERROR', 'Error condition detected!'); // logError :: String -> * const logError = R.curry(logger)('console', 'basic', 'FJS', 'ERROR'); logError('Error code 404 detected!!');
- 最後のパラメータ(メッセージ)を除くすべてのパラメータを部分的に設定しておける
部分適用とパラメータ束縛
カリー化 | 部分適用 | |
---|---|---|
型宣言による比較(3引数) | ((a, b, c) -> d) -> (a -> b -> c -> d) |
(((a, b, c) -> d), a) -> ((b, c) -> d) |
内部 | 多引数関数((a, b, c) -> d) を受け取り、単項関数の入れ子 (a -> b -> c -> d) を返却する |
多引数関数とパラメータ(((a, b, c) -> d), a) を受け取り、固定パラメータ a を含めたクロージャ((b, c) -> d) を返却する |
パラメータを渡すタイミング | カリー化された関数(a -> b -> c -> d) を呼び出すとき |
クロージャ((b, c) -> d) を生成するとき |
呼び出しの引数が足りないと | R.curry(f)(a)(b) は c -> d なる関数を返す |
_.partial(a)(b) は c = undefined としてf が完全に評価される |
- 関数束縛
- JSネイティブサポート
Function.prototype.bind
- オブジェクトのコンテキストで実行可能
bind
の第一引数がthis
に渡される
- JSネイティブサポート
コア言語を拡張する方法としての部分適用
// 文字列の先頭から指定文字数切り出す // first :: Number -> String String.prototype.first = _.partial(String.prototype.substring, 0, _); // _はパラメータ未指定ということを表すためのシンボル(Lodash)
- コア言語が将来更新されてバッティングする可能性があるので注意
- 【補】
Array.prototype
は拡張してはいけない- レガシーブラウザでは拡張メンバを
enumerable: false
にできないためfor in
で列挙されてしまう - そのためか、Babelで
Array.prototype.find
等をトランスパイルしても、Array
が拡張されArray.find
が定義される
- レガシーブラウザでは拡張メンバを
遅延関数に束縛する
console.log
とかwindow.setTimeout
とかはオブジェクトのコンテキストじゃないと動かないよ、という話Function.prototype.bind
や_.bind
を使って、console
やwindow
のコンテキストで動かせ- p.131はたぶん誤訳がある
undefined
でバインドしたら、暗黙裡にグローバルコンテキスト(ブラウザならwindow
)が渡るのでしょう
関数パイプラインを合成する
HTMLウィジェットとの合成を理解する
略
合成関数:記述を評価から分離する
2つの関数の合成の正式な表現
compose:: ((B -> C), (A -> B)) -> (A -> C)
インタフェースに対するプログラミングの原理を満たす
関数ライブラリによる合成
const smartestStudent = R.compose( R.head, R.pluck(0), R.reverse, R.sortBy(R.prop(1)) R.zip); /* R.head :: [a] -> a | Undefined R.pluck :: Functor f => k -> f {k: v} -> f v R.reverse :: [a] => [a] R.sortBy :: Ord b => (a -> b) -> [a] -> [a] R.zip :: [a] -> [b] -> [[a, b]] */ // 全部composeすると // [a] -> [b] -> a | Undefined
- こういうコードになる
- 処理の流れと逆順なのがイヤなら、
R.pipe
を使うべし - 【疑問】なんで
R.head
は[a] -> Maybe a
じゃないんだろう…
純粋なコードと不純なコードを取り扱う
- 純粋な振る舞いと不純な振る舞いの両方があることを許容する
- 「副作用なしでできることと言ったら、ボタンを押して、箱がしばらくあったかくなるのを見守るだけだ」(サイモン・ペイトン・ジョーンズ)
ポイントフリープログラミングの紹介
- 引数を明示的に宣言しないやつ
- tacit programming とも
- Unixのパイプとそっくり
関数コンビネータを使ってフロー制御を管理する
- FPにはif-elseとかがない
- 代わりに関数コンビネータを用いる
identity (Iコンビネータ)
identity :: a -> a
- Haskellの
const
とかでも知られる
用途
- 引数を期待する高階関数にデータを与える
R.sortBy(R.identity)
tap (Kコンビネータ)
- void型関数(副作用メインのやつ)を関数合成に組み込む
tap :: (a -> *) -> a -> a
alternation (ORコンビネータ)
// 生徒を検索し、いなければ生成する
alt(findStudent, createStudent);
sequence (Sコンビネータ)
- 一連の複数の関数を順次実行
- 戻り値なし。関数合成を続けたければKコンビネータを併用せよ
// // String -> * seq(append('#student-info'), consoleLog) // 下記を順次実行 // id=student-infoの要素に文字列追加 // console.log出力
fork(join)コンビネータ
- 処理を
func1
、func2
にフォークし、join
で結合する
fork :: (b -> c -> d) -> (a -> b) -> (a -> c) -> a -> d
function fork (join, func1, func2) { /* ... */ }
- 例: 平均値の算出
// sum :: [Number] -> Number // count :: [Number] -> Number // divide :: Number -> Number -> Number // // average :: [Number] -> Number const average = fork(divide, sum, count)
等式推論
average(arr) fork(divide, sum, count)(arr) divide(sum(arr), count(arr))
まとめ
- 関数チェーンおよび関数パイプラインは、再利用可能かつモジュール化・コンポーネント化された関数を接続する
- Ramdaはカリー化と合成に適した関数型ライブラリであり、ユーティリティ関数として強力な武器を持っている
- カリー化と部分適用は、純粋関数のアリティを減らすのに利用できる。アリティを減らすことは、関数の引数の一部を部分的に評価して、純粋関数を単項関数に変換することによって達成される
- 全体の解決に到達するために、タスクを簡単な関数に分割して、それらの関数を合成することができる
- 関数コンビネータを使用すると、複雑なプログラムフローの調整や、ポイントフリーな方法でプログラムを書くことが可能になり、実世界の任意の問題に挑むことができる