コンピュータサイエンスにおいて、関数型プログラミングとは、関数を適用・合成することでプログラムを構築するプログラミングパラダイムです。関数定義は、プログラムの 実行状態を更新する命令型の文の列ではなく、値を他の値にマッピングする式のツリー構造で表現される宣言型プログラミングパラダイムです。
関数型プログラミングでは、関数は第一級エンティティとして扱われます。つまり、他のデータ型と同様に、関数は名前(ローカル識別子を含む)に束縛され、引数として渡され、他の関数から返されます。これにより、小さな関数をモジュール方式 で組み合わせる、宣言的かつ構成可能なスタイルでプログラムを記述できます。
関数型プログラミングは、すべての関数を決定論的な数学関数、つまり純粋関数として扱う関数型プログラミングのサブセットである純粋関数プログラミングと同義に扱われることがあります。純粋関数は、指定された引数で呼び出されると、常に同じ結果を返し、変更可能な状態やその他の副作用の影響を受けません。これは、副作用(プログラムの状態を変更したり、ユーザーからの入力を取得したりするなど)を持つ可能性がある、命令型プログラミングで一般的な不純な手続きとは対照的です。純粋関数プログラミングの支持者は、副作用を制限することでプログラムのバグが少なくなり、デバッグとテストが容易になり、形式検証に適したものになると主張しています。[ 1 ] [ 2 ]
関数型プログラミングは学術分野に起源を持ち、関数のみに基づいた正式な計算システムであるラムダ計算から発展しました。関数型プログラミングは歴史的に命令型プログラミングほど人気がありませんでしたが、 Common Lisp、Scheme、[ 3 ] [ 4 ] [ 5 ] [ 6 ] Clojure、Wolfram Language、[ 7 ] [ 8 ] Racket 、[ 9 ] Erlang、[ 10 ] [ 11 ] [ 12 ] Elixir、[ 13 ] OCaml、[ 14 ] [ 15 ] Haskell、[ 16 ] [ 17 ] F # [ 18 ] [ 19 ]など多くの関数型言語が今日では産業界や教育の場で使用されています。Leanは数学の定理の検証によく使用される関数型プログラミング言語です。[ 20 ]関数型プログラミングは、WebにおけるJavaScript 、 [ 21 ]統計におけるR 、 [ 22 ] [ 23 ]金融分析におけるJ、K 、 Q 、 XMLのXQuery / XSLTなど、特定のドメインで成功を収めた言語の鍵でもあります。[ 24 ] [ 25 ] SQLやLex / Yaccなどのドメイン固有の宣言型言語は、変更可能な値を許可しないなど、関数型プログラミングの要素の一部を使用します。[ 26 ]さらに、他の多くのプログラミング言語が関数型スタイルでのプログラミングをサポートしているか、関数型プログラミングの機能を実装しています。たとえば、C++ ( C++11以降)、C#、[ 27 ] Kotlin、[ 28 ] Perl、[29 ] PHP、 [ 30 ] Python、 [ 31 ] Go、 [ 32 ] Rust、 [ 33 ] Raku、 [ 34 ] Scala、 [ 35 ] Java( Java 8以降) [ 36 ]
ラムダ計算は、1930年代にアロンゾ・チャーチによって開発された、関数適用から構築された正式な計算体系です。1937年、アラン・チューリングはラムダ計算とチューリングマシンが同等の計算モデルであることを証明し、[ 37 ]ラムダ計算がチューリング完全であることを示しました。ラムダ計算は、すべての関数型プログラミング言語の基礎を形成しています。同等の理論的定式化である組合せ論理は、 1920年代から1930年代にかけてモーゼス・シェーンフィンケルとハスケル・カリーによって開発されました。 [ 38 ]
チャーチは後に、より弱いシステムである単純型付きラムダ計算を開発しました。これは、すべての項にデータ型を割り当てることでラムダ計算を拡張したものです。 [ 39 ]これが静的型付き関数型プログラミングの基礎となります。
最初の高水準関数型プログラミング言語であるLisp は、1950 年代後半にマサチューセッツ工科大学(MIT)在籍中のジョン・マッカーシーによってIBM 700/7000 シリーズの科学計算用コンピュータ向けに開発されました。 [ 40 ] Lisp の関数はチャーチのラムダ記法を使用して定義され、ラベル構造によって再帰関数を可能にするよう拡張されました。[ 41 ] Lisp は関数型プログラミングの多くのパラダイム的な特徴を初めて導入しましたが、初期の Lisp はマルチパラダイム言語であり、新しいパラダイムが進化するにつれて、多数のプログラミングスタイルのサポートを取り入れました。その後のSchemeやClojureなどの方言や、 DylanやJuliaなどの派生言語は、明確に関数型の中核部分を中心に Lisp を簡素化、合理化しようとしましたが、Common Lispは、置き換えた多数の古い方言のパラダイム的な特徴を保持し、更新するように設計されました。[ 42 ]
1956年に開発された情報処理言語(IPL)は、コンピュータベースの最初の関数型プログラミング言語として挙げられることがあります。[ 43 ]これは、シンボルのリストを操作するためのアセンブリ言語です。IPLにはジェネレータの概念があり、これは関数を引数として受け取る関数に相当します。また、低水準プログラミング言語であるため、コードがデータになることもあり、IPLは高階関数を持つと見なすことができます。しかし、IPLは変更可能なリスト構造や同様の命令型機能に大きく依存しています。
ケネス・E・アイバーソンは1960年代初頭にAPLを開発し、1962年の著書『プログラミング言語』(ISBN 978-4-8632-1111)でその詳細を説明しています。 9780471430148(APL)はジョン・バッカスのFPに最も大きな影響を与えました。1990年代初頭、アイバーソンとロジャー・フイはJを開発しました。1990年代半ばには、アイバーソンと以前一緒に働いていたアーサー・ホイットニーがKを開発しました。Kは、その派生であるQと共に金融業界で商業的に利用されています。
1960年代半ば、ピーター・ランディンは関数型プログラミング言語用の最初の抽象マシンであるSECDマシン[ 44 ]を発明し、[ 45 ] ALGOL 60とラムダ計算の対応関係を記述し、[ 46 ] [ 47 ] ISWIMプログラミング言語を提案した。[ 48 ]
ジョン・バッカスは1977年のチューリング賞講演「プログラミングはフォン・ノイマン・スタイルから解放されるか?関数型スタイルとそのプログラム代数」でFPを紹介した。 [ 49 ]彼は関数型プログラムを、「プログラムの代数」を可能にする「組み合わせ形式」によって階層的に構築されるものと定義した。現代の言葉で言えば、これは関数型プログラムが構成性の原則に従うことを意味する。バッカスの論文は関数型プログラミングの研究を普及させたが、それは関数型プログラミングで現在関連付けられているラムダ計算スタイルではなく、 関数レベルのプログラミングを強調していた。
1973年の言語ML は、エディンバラ大学のロビン・ミルナーによって作成され、セント・アンドリュース大学でデビッド・ターナーによって言語SASLが開発されました。同じく1970年代のエディンバラでは、バーストールとダーリントンが関数型言語NPL を開発しました。[ 50 ] NPL はクリーネ再帰方程式に基づいており、プログラム変換に関する研究で初めて導入されました。[ 51 ]その後、バーストール、マッククイーン、サンネラは ML から多態的型チェックを組み込み、言語Hopeを作成しました。[ 52 ] ML は最終的にいくつかの方言に発展し、現在最も一般的なのはOCamlとStandard MLです。
1970年代、ガイ・L・スティールとジェラルド・ジェイ・サスマンは、ラムダ論文集と1985年の教科書『Structure and Interpretation of Computer Programs』で説明されているように、Schemeを開発しました。Schemeは、関数型プログラミングを促進する機能である、レキシカルスコープと末尾呼び出し最適化を必要とする最初のLisp方言でした。
1980年代、ペル・マーティン=レーフは直観主義型理論(構成型理論とも呼ばれる)を考案しました。これは、関数型プログラムを、依存型として表現される構成的証明と関連付ける理論です。これは対話的な定理証明への新たなアプローチにつながり、その後の関数型プログラミング言語の開発に影響を与えました。
デイビッド・ターナーによって開発された遅延関数型言語、ミランダは1985年に登場し、 Haskellに大きな影響を与えました。ミランダはプロプライエタリ言語でしたが、Haskellは1987年に関数型プログラミング研究のためのオープンスタンダードを形成するという合意を得て誕生しました。実装は1990年現在も継続的にリリースされています。
最近では、CGALフレームワーク上に構築されたOpenSCAD言語のパラメトリックCADなどのニッチな分野で使用されていますが、値の再割り当てに関する制限(すべての値は定数として扱われます)により、関数型プログラミングの概念に馴染みのないユーザーの間で混乱が生じています。[ 53 ]
関数型プログラミングは商業的な場面でも引き続き使用されています。[ 54 ] [ 55 ] [ 56 ]
いくつかの概念[ 57 ]やパラダイムは関数型プログラミングに特有のものであり、命令型プログラミング(オブジェクト指向プログラミングを含む)では一般的に馴染みのないものです。しかし、プログラミング言語は複数のプログラミングパラダイムに対応していることが多いため、「主に命令型」の言語を使用するプログラマーは、これらの概念のいくつかを利用している可能性があります。[ 58 ]
高階関数とは、他の関数を引数として取ったり、結果として返したりできる関数です。微積分学では、高階関数の例として、関数 の導関数を返す微分演算子 が挙げられます。
高階関数は第一級関数と密接に関連しており、どちらも関数を他の関数の引数や戻り値として使用できます。両者の違いは微妙です。「高階」は他の関数を操作する関数という数学的な概念を表すのに対し、「第一級」は使用に制限のないプログラミング言語のエンティティを表すコンピュータサイエンス用語です(したがって、第一級関数は、数値などの他の第一級エンティティと同様に、プログラム内のどこにでも記述できます。他の関数の引数や戻り値として記述することもできます)。
高階関数は部分適用、あるいはカリー化を可能にします。これは、関数を引数に一つずつ適用し、適用するたびに次の引数を受け入れる新しい関数を返す手法です。これにより、例えばプログラマは後続関数を、自然数1に部分適用された加算演算子として簡潔に表現できます。
純粋関数(または式)には副作用(メモリやI/Oへの副作用)がありません。つまり、純粋関数にはいくつかの有用な特性があり、その多くはコードの最適化に利用できます。
命令型プログラミング言語用のコンパイラのほとんどは純粋関数を検出し、純粋関数呼び出しの共通部分式除去を実行しますが、プリコンパイルされたライブラリでは必ずしもこれを行うとは限りません。プリコンパイルされたライブラリは通常この情報を公開しないため、外部関数を含む最適化が妨げられます。gccなどの一部のコンパイラは、プログラマが外部関数を明示的に純粋関数としてマークするためのキーワードを追加し、このような最適化を可能にします。Fortran 95でも関数を純粋関数として指定できます。[ 59 ] C++11ではconstexpr同様のセマンティクスを持つキーワードが追加されました。
関数型言語における反復処理(ループ)は、通常、再帰によって実現されます。再帰関数は自身を呼び出し、基本ケースに到達するまで操作を繰り返します。一般的に、再帰はスタックを維持する必要があり、スタックは再帰の深さに比例して空間を消費します。そのため、命令型ループの代わりに再帰を使用すると、コストがかかりすぎる可能性があります。ただし、末尾再帰と呼ばれる特殊な形式の再帰は、コンパイラによって認識され、命令型言語で反復処理を実装するのと同じコードに最適化されます。末尾再帰の最適化は、コンパイル時にプログラムを継続渡しスタイルに変換するなどの方法で実装できます。
Scheme言語標準では、実装が適切な末尾再帰をサポートすることを要求しており、アクティブな末尾呼び出しの数を無制限に許可する必要がある。[ 60 ] [ 61 ]適切な末尾再帰は単なる最適化ではなく、再帰を使用してループを表現できること、およびそうすることで領域が安全になることをユーザーに保証する言語機能である。[ 62 ]さらに、その名前に反して、末尾再帰だけでなくすべての末尾呼び出しを考慮します。適切な末尾再帰は通常、コードを命令型ループに変換することで実装されますが、実装では他の方法で実装することもできます。たとえば、Chicken は意図的にスタックを保持し、スタックがオーバーフローするようにします。ただし、これが発生すると、ガベージコレクターが領域を取り戻し、[ 63 ]末尾再帰をループに変換しないにもかかわらず、アクティブな末尾呼び出しの数を無制限に許可します。
一般的な再帰パターンは高階関数を用いて抽象化することができ、カタモルフィズムとアナモルフィズム(あるいは「フォールド」と「アンフォールド」)が最も分かりやすい例です。このような再帰スキームは、命令型言語におけるループなどの組み込み制御構造と同様の役割を果たします。
ほとんどの汎用関数型プログラミング言語は無制限再帰を許可しており、チューリング完全である。そのため停止問題は決定不能となり、方程式推論の不健全性を引き起こす可能性があり、一般に言語の型システムによって表現されるロジックに矛盾が生じる必要がある。Rocqなどの一部の特殊用途言語は、整列再帰のみを許可し、強く正規化される(停止しない計算は、 codataと呼ばれる無限の値のストリームでのみ表現できる)。結果として、これらの言語はチューリング完全ではなく、特定の関数を表現することは不可能であるが、無制限再帰によってもたらされる問題を回避しながら、幅広い興味深い計算を表現することができる。整列再帰に限定され、他のいくつかの制約がある関数型プログラミングは、完全関数型プログラミングと呼ばれる。[ 64 ]
関数型言語は、厳密評価(積極的評価)と非厳密評価(遅延評価)のどちらを使用するかによって分類できます。これらの概念は、式の評価時に関数の引数がどのように処理されるかを指します。技術的な違いは、失敗する計算や発散する計算を含む式の表示的意味にあります。厳密評価では、失敗する部分項を含む項の評価は失敗します。例えば、次のPython文は、
print ( len ([ 2 + 1 , 3 * 2 , 1 / 0 , 5 - 4 ]))厳密な評価では、リストの3番目の要素がゼロ除算されているため、関数は失敗します。遅延評価では、length関数は4(つまり、リスト内の項目数)を返します。これは、関数の評価ではリストを構成する項を評価しないためです。つまり、厳密な評価では、関数を呼び出す前に関数の引数を常に完全に評価します。遅延評価では、関数の引数の値が関数呼び出し自体の評価に必要でない限り、関数の引数は評価されません。
関数型言語における遅延評価の通常の実装戦略はグラフ削減である。[ 65 ]遅延評価は、 Miranda、Clean、Haskellなどのいくつかの純粋関数型言語でデフォルトで使用されている。
ヒューズ(1984)は、データストリームの生産者と消費者の独立した実装を容易にすることで、関心の分離を通じてプログラムのモジュール性を向上させるメカニズムとして遅延評価を主張している。 [ 2 ] Launchbury(1993)は、特にプログラムの記憶域要件の分析において、遅延評価がもたらすいくつかの困難について説明し、そのような分析を支援する操作的意味論を提案している。 [ 66 ] Harper(2009)は、言語の型システムを使用してそれらを区別し、厳密な評価と遅延評価の両方を同じ言語に含めることを提案している。[ 67 ]
特に1970 年代のHindley-Milner 型推論の開発以来、関数型プログラミング言語は、コンパイル時にすべての無効なプログラムを拒否し、偽陽性エラーのリスクがある型付きラムダ計算を使用する傾向にあり、これは、コンパイル時にすべての有効なプログラムを受け入れ、有効なプログラムを拒否しないだけの情報がある場合に実行時にすべての無効なプログラムを拒否するため、偽陰性エラーのリスクがある型なしラムダ計算とは対照的です。代数的データ型の使用により、複雑なデータ構造の操作が容易になります。強力なコンパイル時型チェックの存在により、テスト駆動開発などの他の信頼性技術がない場合でもプログラムの信頼性が向上します。一方、型推論により、ほとんどの場合、プログラマがコンパイラに対して手動で型を宣言する必要がなくなります。
Rocq、Agda、Cayenne、Epigramなどの研究指向の関数型言語は、型が項に依存できる直観主義型理論に基づいています。このような型は依存型と呼ばれます。これらの型システムには決定可能な型推論がなく、理解してプログラムするのが困難です。[ 68 ] [ 69 ] [ 70 ] [ 71 ]しかし、依存型は高階論理で任意の命題を表現できます。そのため、カリー–ハワード同型性を通じて、これらの言語の型付けされたプログラムは、コンパイラが認証されたコードを生成することができる形式的な数学的証明を書く手段になります。これらの言語は主に学術研究(形式化された数学を含む)で関心を集めていますが、工学でも使用され始めています。Compcertは、言語Cのサブセット用のコンパイラで、 Rocqで書かれ、形式的に検証されています。[ 72 ]
一般化代数データ型(GADT)と呼ばれる依存型の限定的な形式は、依存型プログラミングの利点の一部を提供しながら、その不便さのほとんどを回避する方法で実装することができます。[ 73 ] GADTはグラスゴーHaskellコンパイラ、OCaml [ 74 ]、Scala [ 75 ]で利用可能であり、JavaやC#などの他の言語への追加として提案されています。[ 76 ]
関数型プログラムには代入文がありません。つまり、関数型プログラム内の変数の値は、一度定義されると決して変更されません。これにより、変数は実行時にいつでも実際の値に置き換えられるため、副作用の可能性が排除されます。したがって、関数型プログラムは参照透過的です。[ 77 ]
Cの代入文を考えてみましょうx = x * 10。これは変数 に代入された値を変更しますx。 の初期値がxだったとする1と、変数 を2回連続して評価すると、それぞれ と がx生成されます。明らかに、を または に置き換えるとプログラムに異なる意味が与えられ、したがって式は参照透過的ではありません。実際、代入文は決して参照透過的ではありません。 10100x = x * 1010100
ここで、入力 x を暗黙的に変更しないため、副作用がないため透過的である別の関数を考えてみましょう。関数型プログラムはこの種の関数のみを使用するため、参照透過的です。 intplusOne(intx){returnx+1;}
純粋関数型データ構造は、命令型のデータ構造とは異なる方法で表現されることが多い。[ 78 ]例えば、アクセスと更新時間が一定である配列は、ほとんどの命令型言語の基本構成要素であり、ハッシュテーブルやバイナリヒープなどの多くの命令型データ構造は配列に基づいている。配列は、純粋関数型の実装が可能なマップやランダムアクセスリストに置き換えることができるが、アクセスと更新時間は対数的である。純粋関数型データ構造には永続性があり、これはデータ構造の以前のバージョンを変更せずに保持する特性である。Clojureでは、永続的データ構造は命令型のデータ構造の機能的な代替として使用される。例えば、永続ベクターは部分的な更新のためにツリーを使用する。挿入メソッドを呼び出すと、すべてのノードが作成されるのではなく、一部のノードが作成される。[ 79 ]
関数型プログラミングは命令型プログラミングとは大きく異なります。最も大きな違いは、関数型プログラミングでは副作用を回避できる点にあります。命令型プログラミングでは副作用は状態やI/Oの実装に使用されます。純粋関数型プログラミングは副作用を完全に排除し、参照の透明性を実現します。
高階関数は、古い命令型プログラミングではほとんど使用されません。従来の命令型プログラムでは、リストを走査したり変更したりするためにループが使用されることがあります。一方、関数型プログラムでは、関数とリストを受け取り、各リスト項目に関数を適用することで新しいリストを生成して返す高階「map」関数が使用されることが多いでしょう。
次の 2 つの例 ( Javaで記述) は同じ効果を実現します。配列内のすべての偶数を 10 倍にしてすべてを加算し、最終的な合計を変数に格納しますresult。
従来の命令型ループ:
int [] numList = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; int result = 0 ; for ( int i : numList ) { if ( i % 2 == 0 ) { result += i * 10 ; } }高階関数を使った関数型プログラミング:
java.util.Arraysをインポートします。int [] numList = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; int result = Arrays . stream ( numList ) . filter ( n -> n % 2 == 0 ) . map ( n -> n * 10 ) . reduce ( 0 , Integer :: sum );場合によっては、関数型プログラミングによって提供される抽象化によって、 off-by-one エラーなどの大量の複雑な命令型コードを構築する際に発生する可能性のある特定の問題を回避する、より堅牢なコードの開発が可能になることがあります(グリーンスパンの 10 番目のルールを参照)。
銀行口座の残高管理など、状態を使って実装するのが最も自然なタスクがいくつかあります。純粋関数型プログラミングでは、これらのタスクや、ユーザー入力の受信や画面への出力といったI/Oタスクを、異なる方法で実行します。
純粋関数型プログラミング言語Haskellは、圏論から派生したモナドを用いてこれらを実装している。[ 80 ]モナドは、純粋性を損なうことなく、命令型で変更可能な状態(およびI/Oなどの副作用)を持つ計算のモデル化など(ただしこれに限定しない)、特定の種類の計算パターンを抽象化する手段を提供する。既存のモナドは、適切なテンプレートと例があればプログラムに簡単に適用できるかもしれないが、多くの学生は、例えば新しいモナドを定義するように求められた場合(特定の種類のライブラリでは必要な場合がある)、概念的に理解するのが難しいと感じている。[ 81 ]
関数型言語は、不変の状態を渡すことで状態をシミュレートします。これは、関数に状態をパラメータの一つとして受け入れさせ、結果と共に新しい状態を返すことで実現できます。古い状態は変更されません。[ 82 ]
非純粋関数型言語は通常、可変状態を管理するためのより直接的な方法を備えています。例えば、 Clojureは、現在の状態に純粋関数を適用することで更新できるマネージド参照を使用しています。このようなアプローチは、計算を表現するための好ましい方法として純粋関数の使用を促進しつつ、可変性を実現します。
プログラム内の副作用を追跡するために、ホーア論理や一意性といった代替手法が開発されてきた。現代の研究言語の中には、副作用の存在を明示的に示すために効果システムを使用しているものもある。 [ 83 ]
関数型プログラミング言語は一般に、 CやPascalなどの命令型言語に比べてCPUとメモリの利用効率が低い。[ 84 ] これは、配列などの一部の可変データ構造が、現在のハードウェアを使用して非常に簡単に実装できるという事実に関係している。フラット配列は、深くパイプライン化されたCPUを使用すれば非常に効率的にアクセスできるし、キャッシュを介して効率的にプリフェッチすることも(複雑なポインタ追跡なしで)、SIMD命令で処理することもできる。同様に効率的な汎用の不変配列を作成することも簡単ではない。純粋関数型言語の場合、最悪のケースの速度低下は、使用されるメモリセルの数に対して対数的になる。これは、可変メモリが、アクセス時間が対数的な純粋関数型データ構造(バランスツリーなど)で表現できるためである。[ 85 ]ただし、このような速度低下は普遍的ではない。集中的な数値計算を実行するプログラムの場合、 OCamlやCleanなどの関数型言語は、The Computer Language Benchmarks Gameによると、Cよりもわずかに遅いだけである。[ 86 ]大規模な行列や多次元データベースを扱うプログラムのために、配列関数型言語( JやKなど)は速度の最適化を考慮して設計されました。
データの不変性は、多くの場合、命令型言語では安全ではない仮定をコンパイラに許すことで実行効率を高め、インライン展開の機会を増やすことができる。[ 87 ]永続的な不変データ構造を扱う際に暗黙的と思われるコピーが計算コストが高いように見える場合でも、Clojureなどの関数型プログラミング言語では、形式的に不変なデータ間で安全にメモリを共有するためのメカニズムを実装することでこの問題を解決している。 [ 88 ] Rustは、不変参照[ 89 ]とライフタイムと呼ばれる概念を含むデータ不変性へのアプローチによって他とは一線を画している。[ 90 ]
不変データとアイデンティティと状態の分離、およびシェアード・ナッシング・スキームは、同時実行操作は通常アトミックであり、ロックの必要性を排除できるため、特定の同時実行ハザードのリスクを軽減または排除することで、同時実行プログラミングや並列プログラミングにより適している可能性もあります。 これは、例えばクラスが実装される方法であり、クラスの一部は同時使用に適さない、対応するクラスの不変バリアントです。[ 91 ]関数型プログラミング言語には、多くの場合、共有状態と同期の代わりにメッセージ・パッシング・メカニズムを活用する同時実行モデルがあります(アクター・モデルなど、各アクターは状態、動作、子アクター、およびメッセージ・キューのコンテナーです)。[ 92 ] [ 93 ]このアプローチは、 Erlang / ElixirやAkkaで一般的です。 java.util.concurrent
遅延評価はプログラムを漸近的に高速化する可能性がある一方で、最大で定数倍の速度低下を引き起こす可能性がある(ただし、不適切に使用するとメモリリークを引き起こす可能性がある)。Launchbury 1993 [ 66 ]は遅延評価によるメモリリークに関する理論的な問題について議論しており、O'Sullivan et al. 2008 [ 94 ]はそれらの分析と修正に関する実践的なアドバイスを提供している。しかし、参照解除されたコードとデータを多用する遅延評価の最も一般的な実装は、深いパイプラインと多段キャッシュを備えた最新のプロセッサ(キャッシュミスで数百サイクルのコストがかかる可能性がある)ではパフォーマンスが低下する。
一部の関数型プログラミング言語では、「 map」や「filter 」といった高階関数といった抽象化を、基礎となる命令型演算ほど効率的に最適化できない場合があります。例として、 Clojureで5が偶数かどうかをチェックする以下の2つの方法を考えてみましょう。
(偶数? 5 ) ( .equals ( mod 5 2 ) 0 )Java VMバージョン 22 および Clojure バージョン 1.11.1 で実行されているLeiningen REPL 2.11.2のRyzen 7900X GNU/Linux PCでCriteriumツールを使用してベンチマークしたところ、次のように実装された最初の実装が見つかりました。
( defn even? "n が偶数の場合は true を返し、n が整数でない場合は例外をスローします" { :added "1.0" :static true } [ n ] ( if ( integer? n ) ( zero? ( bit-and ( clojure.lang.RT/uncheckedLongCast n ) 1 )) ( throw ( IllegalArgumentException. ( str "引数は整数でなければなりません: " n )))))の平均実行時間は 4.76 ミリ秒であるのに対し、基礎となるJava.equalsメソッドを直接呼び出す2 番目の平均実行時間は 2.8 μs で、約 1700 倍高速です。その理由の一部は、 の実装に含まれる型チェックと例外処理に起因すると考えられます。例えば、Goのlo ライブラリは、ジェネリクスを使用して関数型プログラミング言語で一般的なさまざまな高階関数を実装しています。ライブラリの作者が提供したベンチマークでは、 の呼び出しは同等のループよりも 4% 遅く、割り当てプロファイルは同じです[ 95 ]。これは、のインライン展開などのさまざまなコンパイラの最適化に起因すると考えられます[ 96 ]。even?mapfor
Rustの特徴的な機能の一つは、ゼロコスト抽象化です。つまり、これらを使用しても実行時に追加のオーバーヘッドが発生しません。これは、コンパイラがループアンローリングを使用することで実現されます。ループアンローリングでは、命令型ループであれ反復子型ループであれ、ループの各反復処理が独立したアセンブリ命令に変換され、ループ制御コードのオーバーヘッドは発生しません。反復操作で配列に書き込む場合、結果の配列要素は特定のCPUレジスタに格納されるため、実行時に定数時間でアクセスできます。[ 97 ]
従来関数型言語とは考えられていない言語でも、関数型プログラミングスタイルを使用することは可能である。[ 98 ]例えば、D [ 99 ]とFortran 95 [ 59 ]はどちらも純粋関数を明示的にサポートしている。
JavaScript、Lua、[ 100 ] Python、Go [ 101 ]は誕生当初からファーストクラス関数を備えていました。 [ 102 ] Pythonは1994年に「 lambda」、「map」、「reduce」、「filter 」をサポートし、Python 2.2ではクロージャもサポートしていましたが、 [ 103 ] Python 3では「reduce」がfunctools標準ライブラリモジュールに移行しました。[ 104 ]ファーストクラス関数は、1994年のPerl 5.0、 PHP 5.3、Visual Basic 9、C# 3.0、C++11、Kotlinなどの他の主流言語にも導入されています。[ 28 ]
Perlでは、lambda、map、reduce、filter、そしてクロージャが完全にサポートされており、頻繁に使用されています。2005年に出版された書籍『Higher-Order Perl』は、関数型プログラミングにおけるPerlの使用に関する包括的なガイドを提供するために執筆されました。
PHPでは、匿名クラス、クロージャ、ラムダが完全にサポートされています。関数型プログラミングを支援するために、不変データ構造のためのライブラリや言語拡張が開発されています。
Javaでは、匿名クラスはクロージャをシミュレートするために使われることがあります。[ 105 ]しかし、匿名クラスは機能が制限されているため、必ずしもクロージャの適切な代替品とは限りません。[ 106 ] Java 8では、一部の匿名クラスの代替としてラムダ式をサポートしています。[ 107 ]
C#では、クロージャとラムダが完全にサポートされているため、匿名クラスは必要ありません。C#の関数型プログラミングを支援するために、不変データ構造用のライブラリと言語拡張が開発されています。
多くのオブジェクト指向設計パターンは、関数型プログラミングの用語で表現できます。たとえば、戦略パターンは単純に高階関数の使用を指示し、ビジターパターンはカタモルフィズムまたはフォールドにほぼ相当します。
同様に、関数型プログラミングの不変データの概念は命令型プログラミング言語にもよく取り入れられており、[ 108 ]例えばPythonのタプルは不変配列であり、JavaScriptのObject.freeze()もその一つである。[ 109 ]
論理プログラミングは関数型プログラミングの一般化と見なすことができ、関数は関係の特殊なケースです。[ 110 ] 例えば、関数 mother(X) = Y (すべての X は唯一の母 Y を持つ)は、関係 mother(X, Y) で表すことができます。関数は引数の入出力パターンが厳密であるのに対し、関係は任意の入出力パターンで問い合わせることができます。次の論理プログラムを考えてみましょう。
母(チャールズ、エリザベス)。母(ハリー、ダイアナ)。このプログラムは関数型プログラムのようにクエリを実行して、子から母親を生成することができます。
?-母(ハリー、X )。X =ダイアナ。? -母(チャールズ、X ) 。X =エリザベス。しかし、逆方向にクエリして子を生成することもできます。
?-母親( X 、エリザベス) 。X =チャールズ。?-母親( X 、ダイアナ) 。X =ハリー。これを使用して、母関係のすべてのインスタンスを生成することもできます。
?-母親( X 、Y )。X =チャールズ、Y =エリザベス。X =ハリー、Y =ダイアナ。関係構文と比較して、関数構文はネストされた関数をより簡潔に記述できます。例えば、関数構文における母方の祖母の定義は、ネストされた形式で次のように記述できます。
母方の祖母( X ) =母(母( X ) )。リレーショナル表記法での同じ定義は、ネストされていない形式で記述する必要があります。
母方の祖母( X 、Y ) :-母( X 、Z )、母( Z 、Y )。ここで は:-を意味し、の場合は , を意味し、は を意味します。
しかし、この2つの表現の違いは単に構文上のものです。Ciao Prologでは、関数型プログラミングにおける関数のように、関係を入れ子にすることができます。[ 111 ]
祖父母( X ) :=親(親( X ) )。親( X ) :=母( X )。親( X ) :=父( X )。母(チャールズ) :=エリザベス。父(チャールズ) :=フィリップ。母(ハリー) :=ダイアナ。父(ハリー) :=チャールズ。?-祖父母( X 、Y )。X =ハリー、Y =エリザベス。X =ハリー、Y =フィリップ。Ciao は関数のような表記法をリレーショナル形式に変換し、標準の Prolog 実行戦略を使用して結果のロジック プログラムを実行します。
高度に拡張可能なテキストエディタファミリーであるEmacsは、プラグインの作成に独自のLisp方言を使用しています。最も人気のあるEmacs実装であるGNU EmacsとEmacs Lispのオリジナルの作者であるリチャード・ストールマンは、Lispを最も好きなプログラミング言語の一つと考えています。[ 112 ]
スプレッドシートは、純粋なゼロ階厳密評価関数型プログラミングシステムの一種と考えることができます。 [ 113 ]しかし、スプレッドシートは一般的に高階関数やコードの再利用性に欠けており、実装によっては再帰も欠いています。高階関数や再利用関数を可能にするためのスプレッドシートプログラムの拡張機能はいくつか開発されていますが、今のところ主に学術的な性質にとどまっています。[ 114 ]
関数型プログラミングパラダイムは、その構成可能性により、マイクロサービスベースのアーキテクチャに適している可能性がある。[ 115 ]
関数型プログラミングは、プログラミング言語理論の分野において活発な研究分野です。関数型プログラミングに特化した査読付きの出版物としては、International Conference on Functional Programming、Journal of Functional Programming、Symposium on Trends in Functional Programmingなど、いくつかあります。
関数型プログラミングは、幅広い産業用途で採用されてきました。例えば、1980年代後半にスウェーデンのEricsson社が開発したErlangは、もともとフォールトトレラントな通信システムの実装に使用されていましたが[ 11 ] 、その後Nortel、Facebook、Électricité de France、WhatsAppなどの企業でさまざまなアプリケーションの構築に人気が高まっています。[ 10 ] [ 12 ] [ 116 ] [ 117 ] [ 118 ] Lispの方言であるSchemeは、初期のApple Macintoshコンピューター上のいくつかのアプリケーションの基盤として使用され[ 3 ] [ 4 ] 、トレーニングシミュレーションソフトウェア[ 5 ]や望遠鏡の制御などの問題に適用されてきました。[ 6 ] 1990年代半ばに導入されたOCamlは、金融分析、 [ 14 ]ドライバー検証、産業用ロボットプログラミング、組み込みソフトウェアの静的解析などの分野で商用利用されてきました。[ 15 ] Haskellは、当初は研究用言語として意図されていましたが、[ 17 ]航空宇宙システム、ハードウェア設計、ウェブプログラミングなどの分野にも応用されています。[ 16 ] [ 17 ]
業界で使用されている他の関数型プログラミング言語には、Scala [ 119 ] F # [ 18 ] [ 19 ] Wolfram Language [ 7 ] Lisp [ 120 ] Standard ML [ 121 ] [ 122 ] Clojure [ 123 ]などがあります。Scalaはデータサイエンスで広く使用されており、[ 124 ] ClojureScript [ 125 ] Elm [ 126 ]またはPureScript [ 127 ]は、プロダクションで使用されている関数型フロントエンドプログラミング言語の一部です。ElixirのPhoenixフレームワークは、 Font AwesomeやAllegro(ポーランド最大のeコマースプラットフォームの1つ)[ 128 ]の分類広告プラットフォームAllegro Lokalnieなど、比較的人気のある商用プロジェクトでも使用されています。[ 129 ]
関数型「プラットフォーム」は、金融分野(特に大手投資銀行)におけるリスク分析に広く利用されています。リスク要因は、市場変動における相関関係を測定するための相互依存グラフ(カテゴリ)を形成する関数としてコード化されます。これはグレブナー基底最適化に類似しているだけでなく、包括的資本分析・審査(CRCA)などの規制枠組みにも適用されています。金融分野におけるOCamlおよびCamlのバリエーションの使用を考えると、これらのシステムはカテゴリカル抽象機械( CAT)に関連すると考えられることがあります。関数型プログラミングは、圏論(Categorical Abstract Machine)の影響を強く受けています。
多くの大学では関数型プログラミングを教えています。[ 130 ] [ 131 ] [ 132 ] [ 133 ]関数型プログラミングをプログラミングの入門概念として扱う大学もあれば[ 133 ]、最初に命令型プログラミング手法を教える大学もあります。[ 132 ] [ 134 ]
コンピュータサイエンス以外では、関数型プログラミングは問題解決、代数、幾何学の概念を教えるために使われています。[ 135 ]また、古典力学の構造と解釈という書籍で古典力学を教えるためにも使われてきました。
特にSchemeは長年にわたりプログラミング教育において比較的人気のある選択肢となってきました。[ 136 ] [ 137 ]
Haskellは、航空宇宙・防衛、金融、Webスタートアップ、ハードウェア設計会社、芝刈り機メーカーなど、幅広い分野で商業的に利用されています。
Scala.
{{cite book}}: CS1 メンテナンス: 場所の発行元が見つかりません (リンク)() メソッドはオブジェクトを凍結します。凍結されたオブジェクトは変更できなくなります。オブジェクトを凍結すると、新しいプロパティの追加、既存のプロパティの削除、既存のプロパティの列挙可能性、設定可能性、書き込み可能性の変更、および既存のプロパティの値の変更ができなくなります。さらに、オブジェクトを凍結すると、そのプロトタイプの変更もできなくなります。freeze() は渡されたオブジェクトと同じオブジェクトを返します。