近年、ハードの発展に伴って関数型言語の名前をよく聞くようになりました。
当時、何も知らなかった自分は「バグが出ないコードが書ける」という噂を聞きつけて、elixirというプログラミング言語に出会いました。その後、Haskellの学習を通じて関数型言語といわれる言語がどのような特徴を備えているのかを身をもって体験しました。
しかし「関数型言語はどんな言語なのか」という問いに対して、WEB上では様々な見解が見られます。
当初、何も知らなかった自分には判断が難しく非常に混乱させられました。
関数型言語では...
- map・filter・reduceを使った実装が関数型言語のプログラミングスタイルだ
- 値は不変(イミュータブル)なもの
- 関数を値として扱うことが出来るの
- 再帰関数を使って繰り返し処理するの
- 副作用がない
- etc ...
様々な考えを持つ人が、異なる理解レベルで情報をまとめることによって様々な見解が生まれるのかと思いました。なら、中立で十分に学習をした人に「関数型言語ってどんな言語?」と聞けないかと考えていたところ、最近、話題の「ChatGPT」のことを思い出しました。
自分もまだまだ修行中の身ですが、ChatGPTに「関数型言語ってどんな言語なのか」と問いかけつつ、その答えについてJavaScriptのサンプルコードを紹介しながら考察してみたいと思います。
ChatGPTに聞いてみた
関数型言語ってどんな言語
関数型言語は、プログラミング言語の一種で、関数を扱うことが特徴的です。関数型言語では、関数を第一級のオブジェクトとして扱うことができ、関数を変数に代入したり、関数を引数に取ったり、関数を戻り値に返すことができます。また、関数型言語では、副作用を最小限に抑え、データを不変にすることが重視されます。代表的な関数型言語には、Haskell、Scala、Clojure、Lispなどがあります。
ここで先ほど紹介した関数型言語の特徴が出てきました。
- 関数を第一級のオブジェクトとして扱うことができる
- 副作用を最小限に抑える
- データを不変にすることが重視される
ここで聞き馴染みのない「関数を第一級のオブジェクト...」という言葉が出てきました。一体、何のことでしょうか。詳しく聞いてみます。
関数を第一級のオブジェクトとして扱うってどういう意味か詳しく教えて
関数を第一級のオブジェクトとして扱うことは、関数を変数に代入したり、関数を引数に取ったり、関数を戻り値に返すことができるという意味です。
例えば、関数型言語では以下のようなことができます。
・ 関数を変数に代入し、変数に格納された関数を呼び出す。
・ 関数を引数に取って、その関数を使って何らかの処理を行う関数を定義する。
・ 関数を戻り値に返すことで、関数を生成したり、関数を組み合わせたりする。
これらの機能により、関数型言語では、高階関数やカリー化など、より柔軟なプログラミングが可能になります。
実はJavaScript
でも関数を第一級のオブジェクトとして扱うことが出来ます。実際に「関数を変数に代入したり、関数を引数に取ったり、関数を戻り値に返すことができる...」のかどうか見てみましょう。
// 関数を変数に代入 const f = (v) => console.log(`output: ${v}`); f('fから与えた値'); // output: fから与えた値 // 関数を引数に取る const g = (f) => f("gから与えた値"); g(f); // output: gから与えられた値 // 関数を戻り値に返す const z = () => f; const zz = z(); zz("zから与えた値"); // output: zから与えた値
上記のサンプルコードではJavaScriptが関数を第一級のオブジェクトとして扱えることを示しています。ではJavaScriptは関数型言語なのでしょうか。ChatGPTに聞いてみます。
JavaScriptは関数型言語ですか
JavaScriptは関数型プログラミングに対応していますが、それはJavaScriptが完全な関数型言語であるわけではありません。JavaScriptはオブジェクト指向プログラミングにも対応しており、関数型プログラミングの機能を使用することができます。
ふむふむ。完全な関数型言語ではないけど、関数型プログラミングの機能を備えているという側面もあるという解釈が正しいようです。では完全な関数型言語はどんな特徴を持っているのでしょうか。
完全な関数型言語について
完全な関数型言語とはなんですか
完全な関数型言語とは、全ての機能が関数型プログラミングに基づいている言語のことを指します。関数型プログラミングには、以下のような特徴があります。
副作用のない関数
純粋な関数
引数として関数を受け取り、関数を返す関数
不変なデータ構造
完全な関数型言語は、これらの特徴をすべて満たしている必要があります。例えば、Haskell, Lisp, Schemeなどが完全な関数型言語になります。
ここで今まで出てこなかった「副作用のない関数」と「純粋な関数」というワードが出てきました。この2つのワードが何を意味しているのか、聞いてみます。
副作用のない関数と純粋関数について教えて
副作用のない関数と純粋関数は、関数プログラミングにおいて重要な概念です。
副作用のない関数は、関数が呼び出された際に、その関数の外部に影響を与えない関数です。つまり、関数が返す値だけが関数の結果であり、その関数が呼び出されたことによって、関数の外部の状態に変化がない関数です。例えば、整数を2倍する関数は、副作用のない関数です。ただし、入力された整数を2倍して、入力された整数を変更する関数は、副作用のある関数です。
純粋関数は、副作用のない関数の一種で、常に同じ入力値に対して同じ結果を返す関数です。純粋関数は、入力値を変更しないため、安全で予測可能なプログラミングを可能にします。また、純粋関数は、並列処理を行うためにも適しています。
副作用のない関数や純粋関数は、関数型プログラミングにおいて重要な概念です。副作用のない関数は、関数が外部の状態に影響を与えないことを意味します。純粋関数は、副作用のない関数の一種で、常に同じ入力値に対して同じ結果を返すことを意味します。
なるほど。ChatGPTの回答を参考にJavaScriptで副作用のある関数と副作用のない関数を書いてみます。
副作用のある関数
let count = 0; const multiple = (n) => { const res = count * n; count++; return res; } console.log(multiple(1)); // 0 console.log(multiple(1)); // 1 console.log(multiple(1)); // 2
関数外のスコープでcountという変数を宣言しており、関数内でインクリメントさせています。
当然、実行されるたびにインクリメントされるため、実行されるごとに結果が変化していきます。つまり「何度実行しても同じ結果が返ってくる」に反しているので、純粋ではない関数です。また関数外、つまり外部に影響を与えているので副作用のある関数となっています。
副作用のない関数
const multiple = (n, count) => n * count; console.log(multiple(1, 0)); // 0 console.log(multiple(1, 1)); // 1 console.log(multiple(1, 2)); // 2
この関数は受け取った2つの引数を乗算します。先ほどとは違いcountのインクリメントは行っていません。与えられた引数が同じ値なら、常に同じ結果を返してくれますし、関数外に何も影響を与えていないので副作用のない純粋な関数です。
JavaScriptは関数型言語なのか
ご覧の通りJavaScriptでは副作用のある・ない関数をどちらも定義できてしまいました。
「副作用のない関数、純粋な関数」の特徴を満たす時もあれば、満たさない時もあるので、JavaScriptは関数型言語の特徴を部分的に備えているけど、完全な関数型言語ではないと理解するのが正しいと思います。
Haskellが純粋関数型言語と呼ばれる一つの理由には「副作用のない関数、純粋な関数」しか定義することができないという点があります。また副作用は最小限に抑えて分離するという方針を強制されます。副作用を扱う時にはアクション、モナドを使いますが、今回の本題ではないため紹介だけに留めておきます。
結論
世の中に多くの言語が登場して様々な意見が飛び交っています。
- 〇〇は関数型言語だ
- △△は関数型言語ではない
しかし、実際には関数型言語であるかどうかという断定をするよりかは、部分的に関数型言語の特徴を備えていると考えるのが良いです。もちろんHaskellのように純粋関数型言語と呼ばれるものは関数型言語というカテゴリーに完全に属するものだと思います。
map, filter, reduceといった関数型言語由来の関数がJavaScriptなど多くの言語で取り入れられたのは、それらが使いやすく良いものだからです。無理にカテゴリー分けするのではなく、良いものは積極的に取り入れていくという気持ちが重要なんだと感じました。