📖

JSで関数型プログラミングを行うために理解すべきっぽいこと

2023/09/04に公開

はじめに

関数型プログラミングの概念がむずすぎだったので、まとめました。
コード・解説はりあクト!TypeScriptで始めるつらくないReact開発 第4版【①言語・環境編】を大いに参考にしています。

関数型プログラミング(functional programming)とは

はっきりとした定義はないが、以下の特徴を持つ言語のことである。

  • プログラムは関数定義の集合であり、関数呼び出しによってそれらを組み合わせる。
  • 関数は第一級オブジェクト(=第一級関数)である。
  • 文という単位は無く、プログラムの実行とは式を評価することである。
  • 参照透過性がある。

第一級オブジェクト(first-class object)

関数を他の関数への引数として渡したり、他の関数から返却したり、変数の値として代入したりすることができるオブジェクトのこと。関数を第一級オブジェクトとして扱うことができるプログラミング言語は、第一級関数をサポートしているといえる。

文(statement)

なんらかの手続きを処理系に命令するもの。
例えば、ifは文であって値としては評価されない。

const rand = Math.floor(Math.random() * 10);
const judge = if(rand % 2 ===0 ) `even` else `odd`;

// Uncaught SyntaxError: Unexpected token 'if'

式(expression)

評価された後に値として存在するもの。
例えば、条件演算子(三項演算子)による評価は式であるから、変数に代入することができる。

const judge = rand % 2 === 0 ? `even` : `odd`;

参照透過性(referential transparency)

数学的な関数と同じように同じ値を返す式を与えたら必ず同じ値を返すような性質。もしf(x)という式の評価結果が2になったとしたら、f(x)を何度呼び出しても常に2を返すことになる。

なお、以下で説明する手続き型言語に参照透過性がない原因は、変数の値が途中で変わることと、式の構成要素以外のもの(グローバル変数など)に依存した計算ができることである。

関数型プログラミングについて、プログラミング・パラダイムの整理を通してさらに理解を進める。

プログラミング・パラダイム

プログラミングのパラダイムは大きく命令型プログラミング宣言型プログラミングに分かれる。

命令型プログラミング

最終的な出力を得るために、状態を変化させる連続した命令文によって記述されるプログラミングスタイルのこと
手順を踏んでいけば結果(=ごはん)が得られるという点で料理のレシピに似ている。
代表的なパラダイムは手続き型プログラミングで、COBOLやBASIC、Pascal、C、Goがこれにあたる。

宣言型プログラミング

出力を得る方法ではなく、出力の性質・あるべき状態を宣言することでプログラミングを構成する方法
もっとも有名な宣言型プログラミングだと、SQLのSELECT文がこれにあたる。

SELECT * FROM employees WHERE employee_number < 1000 AND gender = 'f' ORDER BY hire_date;

なお、オブジェクト指向プログラミングは、命令型か宣言型かという分類からは独立したパラダイムである。

JSで関数型プログラミングをするうえで理解すべきっぽいこと

  1. 無名関数
  2. 第一級関数
  3. 高階関数
  4. 部分適用
  5. 関数合成

1. 無名関数

名前付けされずに定義されるその場限りの関数のこと。

[1,2,3].map((n) => n * 2);    //[ 2, 4, 6 ]

ここでの(n) => n * 2が無名関数となる。

2. 第一級関数

第一級オブジェクトである関数のこと。 関数に引数として関数を渡したり、その戻り値に関数を設定できる。

const double = (n) => n * 2;

変数doubleに無名関数n => n * 2を代入している

3. 高階関数

関数を引数に取る、または戻り値として関数を返すことができる関数のこと。

引数を関数として渡す場合

1.無名関数 のコードを参照。

なお、「コールバック」というのはこの引数として渡される関数のこと。

戻り値を関数として渡す場合

const greeter = (target) => {
    const sayHello = () => {
        console.log(`Hello, ${target}!`);
    };

    return sayHello;
}

const greet = greeter('World');
greet();   // Hello, World!

関数sayHelloが戻り値となっている。

なお、これをアロー関数式に直すと

const greeter = (target) => () => console.log(`Hello, ${target}!`);

となる。

4. 部分適用

カリー化

部分適用について理解するには、まずカリー化についての理解をする必要がある。
カリー化とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること

//カリー化前
{
    const multiply = (n, m) => n * m;
    console.log(multiply(2, 4));   //8
}

//カリー化
{
    const withMultiple = (n) => {
        return (m) => n * m;
    };
    console.log(withMultiple(2)(4));   //8
}

//アロー関数でカリー化
{
    const withMultiple = (n) => (m) => n * m;
    console.log(withMultiple(2)(4));   //8
}

(n, m) => n * mの最初の引数であるnが引数で、戻り値としてmを引数にとり、n * mの計算結果を返す関数withMultipleができている。カリー化とは、複数の引数をとる関数を、より少ない引数をとる関数に分割して入れ子にすること、ともいえる。

部分適用

部分適用とは、カリー化された関数の一部の引数を固定して新しい関数を作ること。

const withMultiple = (n) => (m) => n * m;
console.log(withMultiple(3)(5));   //15

//withMultipleの部分適用
const triple = withMultiple(3);
console.log(triple(5));    //15

withMultipleに3を渡すと、3が入った関数が帰ってくる。それをtripleという変数に保存することで、どんな数を渡しても常に3倍される関数が作れる。これが部分適用である。

5. 関数合成

関数合成とは、各関数の出力を次の関数に渡し、最後の関数の出力を最終的な結果とする、関数を組み合わせるための仕組み。

const compose = (f1, f2) => (x) => f2(f1(x));

const inc = (n) => n + 1;
const double = (n) => n * 2;

const synthesis = compose(inc, double);
console.log(synthesis(2));  //6

まずincに引数2を渡し、結果として帰ってきた3をdoubleに渡すことで、結果6が出力される。

参考

Discussion