テック開発者ブログテコテック開発者ブログ読者になるテコテック開発者ブログ2024-03-07関数型プログラミングのセカイはじめに次世代デジタル基盤開発事業部のIと申します。筆者は、メインの仕事として、Webサイトの設計・実装を行っております。個人的にソースコードの品質を担保する方法を試行錯誤しており、プロジェクトに還元し、そのためのドキュメントをプロジェクトメンバーや部全体に共有しています。今回は、ソースコードの品質担保に大きく寄与するプログラミングパラダイム、関数型プログラミングについて筆者なりの考えなどを共有して、理解の手助けにできればと考えています。前節で、関数型プログラミングを「プログラミングパラダイム」と書きましたが、実質その通りで、プログラムを組むとき、どのように物事(データや処理)を捉えて(詳細)設計や実装を(以降、これを一言で「実装」とまとめます)するかどうかの話です。(以降では、「プログラミングパラダイム」を略してPPと呼ぶようにします)とはいえ、「某PPでは、〇〇を〇〇と捉えている」と書いても客観的過ぎて腑に落ちる感覚がつかめないのも事実です。PPの全体像を論理的に説明しようとしても、全体像が広すぎて収集が付かなくなるリスクがあります。今回説明する関数型プログラミングは、数学や哲学の沼が待ち構えていますので、もっと平易に理解できる観点を持つ必要が出てきます。そこで、本記事では、PPを、アニメやラノベで使われる「世界線」の観点で考えるようにします。「それぞれのPPはそういう世界なんだ」という観念で進めていって、そのうえで論理的な理解を進められるような役割で説明を進めていきます。初音ミク」というリズムアクションゲームが大好きで、毎日タブレットとにらめっこしております。このゲームにはリズムアクションゲームながらストーリーがあり、現実世界(シブヤ)と、登場人物や所属ユニットメンバーの「本当の想い(メンバーたちが心の底からやりたいこと、実現したいこと)」で作られた「セカイ」を通じて彼らの進む道を目指していきます。*1この「セカイ」の概念が、「先人たちが『この処理を忌憚なく実現しよう』『バグのないコードを書けるようにしよう』という想いが募って現実となった」PPの説明と相性が良いので、これからはPPのことを「プログラミングのセカイ(以降、『セカイ』)」と呼ぶようにします。大きなセカイと小さなセカイ前節では、PPを「セカイ」に例えると説明いたしましたが、プロセカのそれとは大きく違うものがあります。プロセカのセカイはそれぞれフラットに存在していますが、プログラミングでのセカイは2階層(上下階層)構造になっています。上層には「大きなセカイ」が3つあり、これらは「データの抽象度」で区別されます。その違いを、以下の図で簡単に列挙します。大まかなプログラミングのセカイ続いて下層には、多数の「小さなセカイ」が存在しています。すべての小さなセカイは、必ず大きなセカイのどれかから紐づいています。プログラミング言語やその処理系は、小さなセカイから一つ、もしくは複数を選び、それ(ら)に則ることで実現すると考えます。小さなセカイの中で代表的なものをピックアップして、一覧形式(下図参照)で列挙します。小さなセカイとその代表例このように、セカイは概念上のものですが、古の言語から最近よく使われるフレームワークまで、言語に囚われない形で実現しています。小さなセカイの表で「後述」としている、関数型のセカイを解説していきたいのですが、系統立てて、関数型のルーツとなる「宣言型のセカイ」を軽く紹介していきます。宣言型のセカイ宣言型プログラミングでは、文字通り「宣言する」ことでプログラムを組んでいきます。変数を宣言するのでしょうか?答えは、「論理」です。数学の証明でよく見かけた、「〇〇は××である」です。宣言型プログラミングでは、数学で証明を記述するかのように、もっと言うと、数学的に正しくプログラムを組んでいくのです。いう言葉を使っていきます)例を挙げて、違いを分かりやすく解説します。表示する」と、コンピューターに命令するように考えて組んでいきます。評価結果は同じである」と考えるのです。表示する」という概念は存在しないと考えます(別の節で、宣言型のセカイでの対応方法を説明しておりますのでご安心を)。命令型のセカイでは、データは具体的に「データ」という認識ですが、宣言型では「何かはわからないけどなんらかの値」「なんらかの値の集合」という、数学らしいふわっとしたもの(数学ではモデルと呼ばれているものです)という認識です。評価結果」であって、「戻り値」という概念も考えないのです(こちらも、別の節で説明します)。関数やイベントドリブンを組み入れたりして評価を始めるきっかけを用意しています。どこから宣言するのか迷うことはありません。ところで、先の説明では「論理」を宣言していくと書いていましたが、論理自体をどのようにプログラムにしていくのでしょうか。これが、小さなセカイへの分岐点となっていくポイントなのです。関数型のセカイでは、宣言を関数で組んでいくセカイなのです。関数型のセカイ前節でも述べたように、このセカイの大きな特徴として、宣言の単位を関数としている点です。このセカイでは、関数を定義・呼び出すことで論理を宣言していき、プログラムとして組み上げます。*4しかし、のべつ幕なしに関数を定義して呼び出すわけにはいきません。数学的な正しさを要求されるからです。そこで、関数型のセカイでは、プログラムを組む際に重要な3つの条件を設けています。これらは、それぞれが違う観点から設けられています。関数定義的観点からの条件:参照透明を遵守する処理系的観点からの条件:評価したい時に評価できる(遅延評価)言語仕様的観点からの条件:関数がファーストクラスオブジェクトであるでは、1番目の「参照透明」から条件を解説していきます。関数定義的観点から・参照透明それでは、参照透明はどういうものなのかを、3つの特性でまとめてみます。実引数が同じならば結果は必ず同じ実引数や関数内で生成した集合は、関数が終わるまで変化しない(=イミュータブル)関数外の要素が結果に依存しない(=副作用が無い)理解を深めるためのサンプルとして、参照透明なプログラムを組んでみます。値が変わることがありません。呼び元の値も変わりませんし、新しい集合(ここでは変数)を変化させません。評価中は関数外の集合に影響されませんし、影響させません。ここでは単純なプログラムのため、他の要因は削るようにしたのですが、本格的に対応しようとすると、命令型に慣れている人にとってかなり戸惑うことになると思います。いうのも、例外発生・画面への出力・ファイル入出力・データベース操作が副作用になるセカイだからです。*5とはいえ、こうなるのはごもっともな話です。数学では関数を評価しても例外をスローしませんし、画面に何も出しません。データベースに何も訊きません。数学的に見ればごもっともですが、コンピューターを扱うならば言語道断な話です。関数型プログラミングではどのように対応しているのでしょうか。裏側を垣間見てみましょう。実は、宣言型を実装できる処理系やフレームワークには、「宣言型のセカイ」と「命令型のセカイ」との橋渡しを用意しています。することで、それぞれのセカイを奇麗に区分けし、それぞれのセカイで忌憚なくプログラミングできるようになります。*6具体的には、以下のようにします。評価結果を処理系や命令型で実装したセカイに丸投げ副作用を含める型を作る処理系に丸投げしている代表例としては、react.jsやVue.jsなどのリアクティブJavascriptフレームワークやライブラリです。Vue.jsのテンプレートやイベントハンドラは命令型で実装することになりますが、必要な値を導き出す関数やcomputed関数を使うは宣言型になります(もっとも、Javascriptなので宣言型を無視できてしまいますが...)。Vuexなどのストアライブラリを併用することによって、参照透明を妨げる状態変数の存在を抽象化できるようになります。副作用を含める型といえば、やはりHaskell由来の「モナド」でしょう。評価結果に「処理系でこの結果を表示する」ことも添えることで、参照透明を壊すことなく画面に結果を表示できますし、ファイルの入出力やデータベースの操作ができるようになります。これらの型が無かったとしても、モナドの思想を反映した型を作ることで参照透明を維持することも考えられます。突き詰めようとすると更に膨大な記事になってしまうので、ここでは「こういうものだ」という認識で進めていきます。宣言を並べるということは、時が流れているということここで次の条件を説明する前に、補足説明をしたいと思います。命令型のセカイでは、複数行のコードは「上から下に手順が記されている」と考えています。時間軸は全く考慮しません。関数型のセカイではどう考えるのでしょうか。百聞は一見にしかず、試しにコードを組んでみます。宣言(理論)で成り立っていますが、(1)~(3)は同一時間に存在しないということに注目します。順に時が流れていると考えます。これで同じものだと認識できるようになります。宣言単体には時の流れが存在しません。言葉を変えると「それぞれの宣言はスナップショットのようなもの」でしょうか。(1)と(2)は命令型のセカイでは代入ですが、関数型のセカイでは等式として認識されます。いうことからも、関数型のセカイでは、右辺と左辺で内容が変わってしまう再代入はありえないものという認識になります。宣言を連ねていくとき、命令型のセカイではよくやる分岐や反復はどう認識すればよいのでしょうか?まず分岐ですが、ここはあまり気にしなくてよいと思います。数学の証明や定義でも、「ただし〇〇のときは××」というように理論を切り分ける場面はたくさんあります。参照透明を遵守する範囲内なら使っても差し支えありません。反復となると厄介です。間違いなく状態変数を使いますし、数学的にも好ましくありません。この点に関しては先人も認識済みで、状態変数を使わない反復を実現するための手助けを備えています。集合の要素を引数とする関数を渡して、新しい集合を返す関数です。いった関数(メソッド)がそれに相当します。反復を処理系に丸投げすることで、宣言型のセカイで反復を実現できるようになっています(参照透明はマストですが)。ここまで参照透明やその背景を見てきましたが、実際問題、いつ、どうやって評価するのでしょうか。そこで、実際の関数評価ー処理系に注目して説明します。処理系的観点から・遅延評価これまでの説明で、命令型に慣れていらっしゃる方々は「命令はすぐに実行されて結果が返る」という考えに慣れきってしまっているために困惑するかと思います。命令型のセカイでは、「要素数が無限の配列」なんて絵空事ですし、実行しようものならすぐにメモリ不足になります。数学の世界では、あらゆるものが抽象的になっているわけですから、「無限」さえも扱えてしまいますし、宣言できてしまいます。いえ、関数型のセカイのプログラムといえども、最終的にコンピューター上で実行できる(≒命令型のセカイで実行する)状態にしないといけません。そこで、関数型のセカイでプログラムを組む時は、評価のタイミングをずらせることが条件となります。関数型のセカイの処理系では「評価したい時に評価する(このことを遅延評価すると言います)」できる土台が準備されているのです。遅延評価できるセカイでは、関数を評価した結果はどう考えるのでしょうか。それは、そのものずばり「関数を評価した結果」と考えます。値は何かとは考えないのです。「1を初期値とした、差1の等差数列の5番目から10番目までを返す」という評価を関数で宣言してみようとします。10)パッと見、命令型のセカイではありえない書き方です。関数の結果が無限の数列なのですから。無限ループになると認識して思考停止します。関数型のセカイでは理に適った内容で、この場合、評価結果は「1,2,3...の数列のうち、5番目から10番目までの結果」です。実際に値が必要な時はどうするのでしょうか。答えは、命令型のセカイで値を取得する関数を呼ぶです。