JavaScript における関数型プログラミングについて

公開日:

目次

JavaScript における関数型プログラミングについて勉強したので、備忘録としてまとめていきます。この記事の内容の多くは、Reactハンズオンラーニング の内容をベースに調べたことをまとめています。この記事を読んでいただき、興味を持った方は Reactハンズオンラーニング の本を読んで頂くことをおすすめいたします。

関数型とは?

一般的に関数型プログラミング言語には以下の7つの特徴があります。

  1. 関数でプログラムを組み立てる
  2. すべての式が値を返す
  3. 関数を値として扱える
  4. 関数と引数を柔軟に組み合わせることができる
  5. 副作用を起こさない
  6. 場合分けと再帰でループ処理を記述する
  7. コンパイラが型を自動的に推測する

関数型プログラミング言語には Haskell や Scala などがあります。(Scalaいつか勉強してみたい…)

これらの特徴の中で、JavaScript が言語の機能としてサポートしている特徴は 1. 関数でプログラムを組み立てる、3. 関数を値として扱える、4. 関数と引数を柔軟に組み合わせることができる、 7.コンパイラが型を自動的に推測する の4つです(1 はやろうと思えば可能というところですが…)。 その中でも JavaScript が関数型プログラミング言語たる所以として最も大切なのは、”3. 関数を値として扱える”という特徴です。

JavaScript では関数は第一級オブジェクトです。これは、かんたんに説明すると、関数を変数と同様に扱うことができる点です。関数を変数と同様に扱うことができるため、JavaScript では関数の引数に関数を渡すことができます。また、ES2015 以降に追加されたアロー関数やPromise、スプレッド構文は JavaScript における関数型プログラミングを更に加速させました。

命令形 vs 宣言型

関数型プログラミングとは、宣言型プログラミングというプログラミングパラダイムの一部です。宣言型プログラミングとは、何をするのか(what)を重要視し、どのようにするのか(how)を重要視しません。つまり、宣言型とはひたすらwhatを記述するプログラミングの手法のことです。一方でオブジェクト指向プログラミングのような従来のプログラミング手法を命令型プログラミングと呼びます。

以下のコードは文字列中の空白を - (ハイフン)に変換する処理を宣言型プログラミングと命令型プログラミングで記述した例になります。

// 宣言型プログラミング

const string = "空白を ハイフンに します";
const newString = string.replace(/ /g, "-");

console.log(newString); // 結果: 空白を-ハイフンに-します
// 命令型プログラミング

const string = "空白を ハイフンに します";

let newString = "";

for (let i = 0; i < string.length; i++) {
  if (string[i] === " ") {
    newString += "-";
  } else {
    newString += string[i];
  }
}

console.log(newString); // 結果: 空白を-ハイフンに-します

上記2つのソースコードを比較すると、宣言型プログラミングのほうが簡潔で読みやすいと思います。宣言型プログラミングでは、何をするのかだけが記述されているためです。

関数型プログラミングの基本概念

関数型プログラミングの基本概念について説明します。

イミュータブルなデータ

イミュータブル とは変更がないという意味で、変更不可能な状態のことを言います。イミュータブルなデータのみを使用することで、意図しないデータを扱わないようにします。ただ実際にはデータの値を変更したいという場合がありますので、関数型プログラミングでは、そういったときの解決策として、変更した値のコピーを作成して、作成したコピーの方の値を変更します。具体的にコードを見てみましょう。

const label = {
  color: 'red',
  text: 'エラー'
}

const changeLabelColor = color => ({...label, color});

const blueLabel = changeLabelColor('blue');
console.log(blueLabel); //  結果: { color: 'blue', text: 'エラー' }
console.log(label); //  結果: { color: 'red', text: 'エラー' }

上記のように、JavaScript ではスプレッド構文を利用して、値をコピーして、コピーした値を変更することでイミュータブルなデータを実現しています。この場合元の値には変更を加えることはありません。changeLabelColor のようにもとの値にエンクを加えることがない関数のことを非破壊的であるといいます。

純粋関数

引数の値のみを参照して、それをもとに計算し値を返す関数を純粋関数と呼びます。副作用のない関数と言い換えることもできます。なぜ純粋関数を使うかというと、テストが簡単になるからです。純粋関数は引数の値だけを見ているため、引数のバリエーションだけを考えることができれば良いことになります。

先程の changeLabelColor 関数は純粋関数ではあると思うのですが、少し微妙な気がします。label オブジェクトが定義されていることが前提となるからです。以下のように書き換えることでより関数の独立性を高めることができるでしょう。

const changeLabelColor = (label, color) => ({...label, color});

関数を純粋関数にすることを意識することで、テストのしやすさやコードの見通しの良さといった様々なメリットをもたらすことができます。これから JavaScript で関数を書くときは次のことに気をつけましょう。

  1. 関数は少なくとも一つの引数を受け取らなければならない
  2. 関数は値もしくは他の関数を戻り値として返却しなければならない
  3. 関数は引数や関数外で定義された変数に直接変更を加えてはならない

データの変換

関数型プログラミングでは、アプリケーションのデータ自体は先程も述べたようにイミュータブルになるため、データの変更は、変更元データのコピーを作成し、そのコピーしたデータを加工するという手順を踏みます。JavaScript にはそのための非破壊的なビルトイン関数が用意されています。Array.map や Array.filter` などが該当します。

const arr = [1, 4, 9, 16];

// Array.map

const map = arr.map(x => x * 2);
console.log(map); // 結果: [2, 8, 18, 32]

// Array.filter

const filter = arr.map(x => x > 2);
console.log(filter); // 結果: [8, 18, 32]

Array.mapArray.filter などのビルトイン関数を正しく利用して記述されたコードは、必然的に宣言的でシンプルになります。

高階関数

高階関数とは、他の関数を引数に取るか、関数を戻り地として返すか、その両方を満たしている関数になります。

関数を引数に取る高階関数とは、データの変換のところで紹介した Array.map や Array.filterなどのことを指します。また、Web制作でよく使うであろうAddEventListener` も高階関数になります。関数を引数に渡す関数を高階関数と呼び、渡された関数のことをコールバック関数と呼ぶわけですね。

自分で関数を引数に取る高階関数を実装すると以下のようになります。

const invokeIf = (condition, fnTrue, fnFalse) => condition ? fnTrue() : fnFalse();

invokeIf(true, () => console.log('Trueです。!'), () => console.log('Falseです。!')); // 結果:Trueです。!
invokeIf(true, () => console.log('Falseです。!'), () => console.log('Falseです。!')); // 結果:Falseです。!

高階関数を使用した場合は、処理の責務と処理の呼び出し責務・処理をした結果の責務を分離することができます。上記の例では invokeIf 関数は condition を条件としてしていますが、condition の条件による振る舞いについては関心を持ちません。

次に、関数を戻り値として返す高階関数についてです。関数を戻り値として返すことで、引数の共通化を図れると行ったメリットがあります。引数の共通化を目的として高階関数を使用する場合、 カリー化 というテクニックが使用されます。以下はカリー化の例になります。

// カリー化の例
const add = x => y => x + y;

const add1 = add(1);

add1(2)
// ⇛ 3
add1(3)
// ⇛ 4
add1(100)
// ⇛ 101

add 関数は簡単なな足し算をするという関数ですが、const add1 = add(1);add 関数を実行する時点では、戻り値として足し算の処理結果を返すのではなく、関数を返却しています。こうすることで、引数を共通化することができます。また、処理のタイミングを呼び出し元に委ねることができる点もカリー化というテクニックを使用するメリットになります。

再帰

再帰とは関数の中から自分自身を呼び出すテクニックです。関数型プログラミングでは再帰を使用してループ処理を記述することが多いため、関数型プログラミングでは再帰を多用します。以下の例では値をカウントダウンする関数において再帰が使用されています。

const countdown = (value, fn) => {
  fn(value);
  return value > 0 ? countdown(--value, fn) : value;
}

countdown(10, (value) => console.log(value));

上記の、 countdown 関数は数値とコールバック関数を引数にとります。countdown 関数の実行時には、まずコールバック関数を実行し、その後、数値が0より大きければデクリメントした上で再帰的に自分自身(countdown 関数)を実行します。

また、再帰は非同期処理と組み合わせた場合に真価を発揮します。非同期処理の場合、再帰呼び出しはすぐには実行されず、例えばデータを取得できたときや、タイマーが発火した際に呼び出されます。以下は countdown 関数に引数 delay を加えて非同期バージョンにしたものです。

const countdown = (value, fn, delay = 1000) => {
  fn(value);
  return value > 0 ? setTimeout(() => countdown(--value, fn, delay), delay) : value;
}

countdown(10, (value) => console.log(value));

上記の例では数値が1秒おきにデクリメントされて出力されます。

このように関数型プログラミングでは for 文や while 文のようなループ命令を使用せず、再帰を使用してループ処理を記述します。理由は副作用を起こさないことを美学とする関数型プログラミングではループ制御のためのカウンタや終了判定のためのカウンタを制御できないことがあるためです。また、コンピュータに繰り返しの実行を指示するする for 文や while 文は「値を返す式」を基本とする関数型プログラミングとは相性が悪いためです。関数型プログラミング言語には for 文や while 文が機能として提供していない場合もあります。関数型プログラミングでは再帰を使用することで簡潔かつ明快にループ処理を記述することができます。

関数の合成

関数型プログラミングではロジックは細分化され、一つ一つの関数は一つのタスクのみを受け持つように実装されます。そして最終的にそれらの関数を組み合わせてアプリケーションを作成します。この場合、関数を上から順番に実行したり、並列に呼び出したり、いくつかの関数呼び出しを束ねてより大きな関数を作成することでアプリケーション全体を構築します。これを関数の合成と呼びます。

関数の合成にもいろいろな方法がありますが、今回は代表的な「メソッドチェーンを利用する方法」と「可変長引数の高階関数を使用する方法」の2つを紹介します。

1つ目の「メソッドチェーンを利用する方法」というのはは jQuery などでよく見られるドット演算子( . )を使って連鎖的に関数呼び出しを行う方法のことです。例えば以下コードでは要素の追加をメソッドチェーンを使用して実装しています。 appendChild関数は追加した子要素が戻り値となるため、DocumentFragment に liタグを追加し、その liタグの子要素に aタグを追加し、そのa タグの子要素に img タグを追加するということをしています。

const frag = document.createDocumentFragment()
const li = document.createElement('li');
const a = document.createElement('a')
const img = document.createElement('img')
frag.appendChild(li).appendChild(a).appendChild(img) // メソッドチェーン

次に、2つ目の「可変長引数の高階関数を使用する方法」を紹介します。以下のように compose という名前の高階関数を定義し、関数を合成します。

const compose = (...fns) => ags =>
  fns.reduce((composed, fn) => fn(composed), ags);

const value = compose(
  civilianHours,
  appendAMPM
)(new Date());

このような書き方であれば、合成する関数の数が増えた場合でも大丈夫です。compose 関数は任意の数の関数を引数にとります。(...fns) という書き方で任意の数の引数を取ることができるようになります。このように引数の数が決まっていないことをを可変長引数と呼びます。その後、 compose 関数は戻り値として関数を返します。戻り値として返された関数は単一の引数 arg を取ります。戻り値の関数を呼び出すことで Array.reduce 関数が呼び出され、引数で受け取った fns という配列内の関数が順番に呼び出されます。配列内の最初の関数は引数 arg で呼び出され、それ以降は1つ前に実行された関数の戻り値が次の関数の引数(composed)として渡されます。このようにして、fns 内の関数が順番に実行され、最後の関数の戻り地が最終的な戻り値として返却されます。

また、上記の compose 関数では引数を1つだけ取る関数のみを対象としています。そのため、引数を2つ取る関数を渡すことはできません。そういったケースに対応できる compose 関数を以下にに実装してみました。

const compose = (...fns) => (...args) =>
  fns.reduce(
    (composed, fn) => fn(...(Array.isArray(composed) ? composed : [composed])),
    args
  );

関数型プログラミングでは、このような合成を使うことで簡潔で、保守性の高いソースコードを記述します。

まとめ

今回の記事では JavaScript における関数型プログラミングについてを書きました。React や Vue などのモダンな JavaScript ライブラリは宣言的であることを重要視されて作られています。そういった点からも関数型プログラミングを理解することはソースコードを書く上で重要であると思います。また、純粋な JavaScript でコードを書く際にもソースコードを簡潔かつ明快に記述するための考え方が関数型プログラミングには詰まっているのではないかと思います。少しでも良いので自分が記述するソースコードに取り入れていけたら良いなと思いました。