JavaScript関数型プログラミング Ch.4 モジュール化によるコードの再利用

JavaScript勉強メモ関数型プログラミング

出典: 

まえがき

  • 前章: 単一のラッパーオブジェクト(_.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とも)

    • gfの返却値を受け取るために、少なくとも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に渡される

コア言語を拡張する方法としての部分適用

// 文字列の先頭から指定文字数切り出す
// 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を使って、consolewindowのコンテキストで動かせ
  • 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)
  • 関数コンビネータのフローに対してユニットテスト

    • 第6章にて
  • カプセル化した型からデータを関数的に抽出する

    • 第5章にて

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)コンビネータ

  • 処理をfunc1func2にフォークし、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はカリー化と合成に適した関数型ライブラリであり、ユーティリティ関数として強力な武器を持っている
  • カリー化と部分適用は、純粋関数のアリティを減らすのに利用できる。アリティを減らすことは、関数の引数の一部を部分的に評価して、純粋関数を単項関数に変換することによって達成される
  • 全体の解決に到達するために、タスクを簡単な関数に分割して、それらの関数を合成することができる
  • 関数コンビネータを使用すると、複雑なプログラムフローの調整や、ポイントフリーな方法でプログラムを書くことが可能になり、実世界の任意の問題に挑むことができる