JavaScript関数型プログラミング Ch.1 関数型で思考する

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

出典: 

Becoming functional

  • オブジェクト指向は可動部をカプセル化することでコードを理解しやすくする。
    関数型プログラミングは可動部を極力減らすことでコードを理解しやすくする。
  • OO makes code understandable by encapsulating moving parts.
    FP makes code understandable by minimizing moving parts.

まえがき

  • 下記の1つでも「はい」or「わからない」なら、関数型プログラミングはまさに必要とされているパラダイム

    • 拡張性

      • 機能を追加するために常にコードをリファクタリングしているか?
    • モジュラー化のしやすさ

      • あるファイルに変更を加えたら、他のファイルにも影響するか?
    • 再利用性

      • コードの重複が多すぎないか?
    • テスト性

      • 関数のユニットテストに苦労していないか?
    • 把握のしやすさ

      • コードが構造化されておらず、理解しにくくないか?
  • 【所感】全部「はい」でした…

関数型プログラミングとは何か

  • 目指すところ

    • 副作用を避ける
    • 状態遷移を減らすためにデータに関する制御フローと処理を抽象化する
  • 基礎的な概念

    • 宣言型プログラミング
    • 純粋関数

      • 副作用と状態変化を伴わない関数
    • 参照透過性
    • 不変性

関数型プログラミングは宣言型である

  • 処理がどのように実装されているか、データがどのように流れるかを明示することなく、一連の処理を表現する
  • 集合型言語と称されるSQLなんかもそう

処理がどのように実装されているか、データがどのように流れるかを明示している例

命令型、手続型とかいわれるやつ

var array = [0, 1, 2, 3, 4];
for (let i = 0; i < array.length; i++) {
    array[i] = Math.pow(array[i], 2);
}
array;
  • やりたいことは「[0,1,2,3,4]の各要素を2乗した配列を得る」こと

    • ここではインデクスとかループとか一切出てこない
  • このコードは、ループのカウンタ、配列のインデックスを適切に管理する責任を負っている

    • やりたいことと関係ないことを強いられている

宣言的である例

[0, 1, 2, 3, 4].map( num => Math.pow(num, 2) );
  • ループのカウンタと配列のインデックスを適切に管理する責任がなくなった
  • ループを再利用できている

純粋関数と副作用問題

純粋関数

  • 戻り値が入力値(引数)にのみ依存

    • 下記には依存しない

      • 隠された値
      • 外部スコープの値
  • スコープ外の値を変更しない

    • グローバル
    • 参照渡しされた引数

(外から見える)副作用の例

  • グローバルに存在する変数やプロパティ、データ構造を変更する
  • 関数の引数の元の値を変更する
  • ユーザー入力を処理する
  • 例外を送出する
  • 画面表示やログ出力
  • HTMLドキュメント、cookie、DBへの問い合わせ

実用性

  • 一切副作用がないプログラムはない

    • 「副作用なしでできることと言ったら、ボタンを押して、箱(=PC)がしばらくあったかくなるのを見守るだけだ」(サイモン・ペイトン・ジョーンズ)

      • Land of Lisp の一節
  • 重要なのは、純粋な部分と不純な部分とを分けること

副作用のある関数の分析

// ssnで学生のレコードを検索し、ブラウザに表示する
function showStudent(ssn) {
    let student = db.find(ssn);                 // スコープ外のdbモジュールへのアクセス
    if (student !== null) {                     // nullチェック (Maybeモナド等で回避したいですね)
        document.querySelector(`#${elementId}`) // スコープ外のelementIdへのアクセス
            .innerHTML =                        // DOM自体が関数外のグローバルなリソース
            `${student.ssn},                    // CSVフォーマットがハードコーディングされている
             ${student.firstname},
             ${student.lastname}`;
    } else {
        throw new Error('Student not found!');    // スコープ外への例外送出
    }
}

showStudent('444-44-44444');
  • パラメータ以外に依存しているので修正やテストがしづらい
  • 役割大杉

単一責務の小さな関数に分割

個々の関数の実装略

var showStudent = compose(        // 以下の関数を合成
        append('#student-info'),  // 3. 指定のDOM要素に書き出す
        csv,                      // 2. csvフォーマットに成形
        find(db)                  // 1. 明示的に渡したdbモジュールで学生の情報を検索
    );
//
// append(selector, str)
// csv(student_info)
// find(db, ssn)
//
// なので、部分適用されて全部1引数関数になっている
//
// 1引数関数を合成したので、全体としても1引数関数


showStudent('444-44-4444');

参照透過性と代替性

参照透過: 同じ入力に対して常に同じ結果を返す

参照透過じゃない例

var counter = 0;

function increment() {
    return ++counter;
}
  • 呼ぶたびに戻り値が変わる

    • スコープ外のcounterに依存

参照透過な例

function increment(counter) {
    return counter + 1;
}

引数がきまれば戻り値もきまる

参照透過なら、等式推論に基づいて書き換えが可能

var input = [80,90,100];
var average = arr => divide(sum(arr), size(arr));
average(input);    // 90

divide(dividend, divisor), sum(arr), size(arr) の実装は枝葉末節なので略

インライニングで書き換えるとこうなる

average([80,90,100]);

divide(sum([80,90,100]), size([80,90,100]));

divide(270, 3);  // 90

副作用があると等式推論は成り立たない

var counter = 0;

// 副作用のある関数
function increment() {
    return ++counter;
}

/* ... */  // counterをmodifyしてるかもしれない

// counterの値は不明
increment();
increment();
print(counter);    // ???

【補】マルチスレッドだと

counter = 0;
increment();  // このへんで別スレッドが counter = 3; とかしない保証はない
increment();
print(counter);    // ???
  • JavaScriptは言語仕様上シングルスレッドだが…
  • マルチスレッドの場合は上記コードでも 2 が得られる保証はないだろう

データの不変性を維持

(外から見える)副作用の例(再掲)

  • 関数の引数の元の値を変更する

不変でないデータを迂闊に扱って副作用を生じてしまう例

function reversed (arr) {
    return arr.reverse();
}
  • JavaScriptにおいて、Arrayオブジェクトは不変でない(mutable)
  • Array.prototype.reverseは、メソッドを呼び出す配列自身をmodifyしてしまう!

    • Array.prototype.sort(comp)とかもそう

これは言語自体の重大な欠点です。(p.19)

【所感】もっと言ってやってくれ

まとめ

  • 関数型プログラミングとは、純粋関数を宣言的に評価することである
  • 純粋関数は、外部から観測可能な副作用を回避することで、不変性を持つプログラムを生成する
  • 関数を、不変性をもつパッケージ化された処理単位として認識することで、多くのバグの可能性をつぶせる
  • 複雑性を克服することにつながる

関数型プログラミングの利点

タスクをシンプルな関数に分解する

  • 処理を単一責任の関数に分割
  • 関数を合成(compose)する

    • compose(f, g)(x) := f(g(x))
    • composeは関数を引数にとる関数 … 高階関数(higher order function)
    • 【補】引数にとられる関数は第一級オブジェクトなので、第一級関数とよばれる
  • 合成以外にも処理を接続する方法はある

    • 【所感】Applicative や Monad のことかな?

円滑なチェーンを使ってデータを処理する

  • ありもののライブラリ

    • Lodash
    • Ramda
    • jQuery
  • 遅延評価

    • value()を呼んで初めて関数チェーンが評価される
    • call-by-need (必要呼び)

リアクティブパラダイムを使ってイベント駆動コードの複雑さを低減する

  • 非同期アプリケーションの複雑性

    • コールバック地獄
  • リアクティブプログラミングというパラダイムでこれに対処

    • コードの抽象度を上げることで、ビジネスロジックに集中できるようにする
    • 非同期やイベント駆動の定型文コードからの解放
    • 関数型プログラミングと組み合わせて、関数型リアクティブプログラミング(FRP)になる
  • ありもののライブラリ

    • RxJS
  • 入力を抽象化

    • 要素のコレクション
    • ユーザ入力のイベント(Observable)

まとめ

  • 純粋関数 => 副作用がない => コードのテストや保守が簡単
  • FP => 宣言型 => 把握しやすい

    • 小さな関数を合成することで読みやすく

      • 第一級関数を高階関数で合成
    • 再利用性が高まりコード量が減る
  • コレクションのデータ処理は、mapreduce等のチェーンで円滑に実行される
  • 関数をビルディングブロックとして扱う

    • ロジックや機能ごとに分類し、まとめたもの
    • コードのモジュール性や再利用性を高める
  • FRP: 関数型リアクティブプログラミング

    • リアクティブプログラミングと関数型プログラミングとを組み合わせて、イベント駆動のプログラムの複雑性を低減
  • OOとFPは二者択一・排他的なものではない