関数型プログラミング、宣言型プログラミング、命令型プログラミングという用語は何を意味しますか?
14 に答える
これを書いている時点で、このページで投票された上位の回答は不正確であり、ウィキペディアを引用した回答を含め、宣言型と命令型の定義が混同されています。いくつかの回答は、さまざまな方法で用語を混同しています。
数式がセルを変更するにもかかわらず、スプレッドシート プログラミングが宣言型である理由についての私の説明も参照してください。
また、いくつかの回答は、関数型プログラミングは宣言型のサブセットでなければならないと主張しています。その点で、「機能」と「手順」を区別するかどうかによって異なります。最初に命令型と宣言型を処理しましょう。
宣言式の定義
宣言式と命令式を区別できる唯一の属性は、そのサブ式の参照透過性 (RT) です。他のすべての属性は、両方のタイプの式で共有されるか、RT から派生します。
100% 宣言型言語 (つまり、すべての可能な式が RT である言語) では、(他の RT 要件の中でも) 格納された値 (HTML やほとんどの Haskell など) の変更は許可されません。
RT式の定義
RT は「副作用がない」とよく言われます。効果という用語には正確な定義がないため、「副作用なし」が RT と同じであることに同意しない人もいます。RT には正確な定義があります。
e
すべてのプログラムで、 inp
のすべての発生をの評価結果に置き換えることができ、観察可能な の結果に影響を与えない場合、式は参照透過的です。e
p
e
p
すべてのサブ式は概念的には関数呼び出しであるため、RT では、関数の実装 (つまり、呼び出された関数内の式) が、関数の外部にある変更可能な状態にアクセスできないことが必要です (変更可能なローカル状態へのアクセスは、許可された)。簡単に言えば、関数 (実装) はpureでなければなりません。
純関数の定義
純粋な関数には「副作用がない」とよく言われます。効果という用語には正確な定義がないため、同意しない人もいます。
純粋関数には次の属性があります。
- 観測可能な唯一の出力は戻り値です。
- 唯一の出力依存関係は引数です。
- 引数は、出力が生成される前に完全に決定されます。
RT は式 (関数呼び出しを含む) に適用され、純粋性は関数 (の実装) に適用されることに注意してください。
RT 式を作成する不純関数のあいまいな例は同時実行ですが、これは割り込み抽象化レイヤーで純粋性が壊れているためです。これは特に知る必要はありません。RT 式を作成するには、純粋な関数を呼び出します。
RTの派生属性
ウィキペディアで使用されている1999 年の引用など、宣言型プログラミングで引用されているその他の属性は、RT から派生するか、命令型プログラミングと共有されます。したがって、私の正確な定義が正しいことを証明しています。
外部値の不変性は、RT の要件のサブセットであることに注意してください。
宣言型言語には、
for
and などのループ制御構造がありません。これは、不変性while
により、ループ条件が変更されないためです。宣言型言語は、ネストされた関数の順序 (別名、論理的な依存関係) 以外の制御フローを表現しません。これは、 不変性により、他の評価順序の選択によって結果が変化しないためです (以下を参照)。
宣言型言語は、論理的な「ステップ」(ネストされた RT 関数の呼び出し順序) を表現しますが、各関数呼び出しがより高いレベルのセマンティック (つまり「何をするか」) であるかどうかは、宣言型プログラミングの要件ではありません。命令との違いは、不変性(より一般的には RT) のため、これらの「ステップ」は可変状態に依存できず、表現されたロジックの関係順序 (つまり、関数呼び出しのネストの順序、別名部分式) のみに依存することです。 )。
たとえば、HTML 段落は、段落<p>
内のサブ式 (タグなど) が評価されるまで表示できません。変更可能な状態はなく、タグ階層の論理関係による順序の依存関係のみがあります (同様にネストされた関数呼び出しであるサブ式のネスト)。
- したがって、不変性 (より一般的には RT) の派生属性があり、宣言式は、構成部分 (つまり、部分式関数の引数)の論理関係のみを表現し、可変状態の関係は表現しません。
評価順
サブ式の評価順序の選択は、関数呼び出しのいずれかが RT ではない (つまり、関数が純粋でない) 場合にのみ、さまざまな結果を与えることができます。たとえば、関数の外部にある変更可能な状態が関数内でアクセスされます。
たとえば、いくつかのネストされた式が与えられた場合f( g(a, b), h(c, d) )
、関数の引数の熱心な評価と遅延評価は、関数f
、g
、およびh
が純粋な場合、同じ結果を返します。
一方、関数f
、g
、およびh
が純粋でない場合、評価順序の選択によって異なる結果が得られる可能性があります。
ネストされた式は、概念的にネストされた関数であることに注意してください。これは、式の演算子は、単項接頭辞、単項接尾辞、またはバイナリ中置記法を装った単なる関数呼び出しであるためです。
a
接線的に言えば、b
、c
、などのすべての識別子d
がどこでも不変であり、プログラムの外部の状態にアクセスできず (つまり、I/O)、抽象化レイヤーの破損がない場合、関数は常に純粋です。
ちなみに、Haskell の構文はf (g a b) (h c d)
.
評価注文の詳細
関数は、入力から出力への状態遷移 (変更可能な格納値ではない) です。純粋な関数への呼び出しの RT 合成では、これらの状態遷移の実行順序は独立しています。各関数呼び出しの状態遷移は、副作用がないことと、RT 関数がそのキャッシュされた値に置き換えられるという原則により、他のものとは独立しています。よくある誤解を正すために、 Haskell のモナドは間違いなく不純であり、したがってプログラムの外部の状態に関して命令的であるという事実にもかかわらず、純粋なモナド構成は常に宣言的かつ RTです (ただし、以下の警告の意味では、副作用隔離されています)。IO
World
一括評価とは、関数が呼び出される前に関数の引数が評価されることを意味し、遅延評価とは、関数内でアクセスされるまで (およびその場合)引数が評価されないことを意味します。
定義: 関数パラメーターは関数定義サイトで宣言され、関数引数は関数呼び出しサイトで提供されます。パラメータと引数の違いを理解してください。
概念的には、すべての式は関数呼び出し (の構成) です。たとえば、定数は入力のない関数、単項演算子は 1 つの入力を持つ関数、二項中置演算子は 2 つの入力を持つ関数、コンストラクターは関数、さらには制御ステートメント (例: if
、)ですfor
。while
関数でモデル化できます。これらの 引数関数(ネストされた関数の呼び出し順序と混同しないでください) が評価される順序は、構文によって宣言されf( g() )
ていません。g
f
g
f
g
f
注意、チューリングの完全な言語 (つまり、無制限の再帰を許可する言語) は完全に宣言的ではありません。たとえば、遅延評価はメモリと時間の不確定性を導入します。ただし、評価順序の選択によるこれらの副作用は、メモリ消費、実行時間、レイテンシ、非終了、外部ヒステリシス、つまり外部同期に限定されます。
関数型プログラミング
宣言型プログラミングにはループを含めることができないため、反復する唯一の方法は関数再帰です。この意味で、関数型プログラミングは宣言型プログラミングに関連しています。
しかし、関数型プログラミングは宣言型プログラミングに限定されません。機能合成は、サブタイプの追加または機能分解のいずれかによって拡張を達成できる表現問題に関して、サブタイプと対比することができます。拡張では、両方の方法論を組み合わせることができます。
関数型プログラミングは通常、関数を第一級のオブジェクトにします。つまり、関数型は、他の型が存在する可能性がある文法のどこにでも出現できます。つまり、関数は関数を入力して関数を操作できるため、関数の構成を強調することで関心の分離を実現できます。つまり、決定論的計算のサブ計算間の依存関係を分離できます。
たとえば、関数型プログラミングでは、コレクションの各要素に適用できる無限の数の特殊なアクションのそれぞれについて個別の関数を作成する(関数も宣言型でなければならない場合は、ループの代わりに再帰を使用する) 代わりに、再利用可能な反復を使用します。関数、例えばmap
、、、。これらの反復関数は、第一級の特殊なアクション関数を入力します。これらの反復関数は、コレクションを反復し、要素ごとに入力専用アクション関数を呼び出します。これらのアクション関数は、コレクションを反復するためのループ ステートメントを含める必要がなくなるため、より簡潔になります。fold
filter
ただし、関数が純粋でない場合、それは実際には手続きであることに注意してください。おそらく、純粋でない関数を使用する関数型プログラミングは、実際には手続き型プログラミングであると主張できます。したがって、宣言式が RT であることに同意する場合、手続き型プログラミングは宣言型プログラミングではないと言えます。したがって、関数型プログラミングは常に RT であり、宣言型プログラミングのサブセットでなければならないと主張するかもしれません。
並列処理
この一流関数による機能合成は、独立した関数を切り離すことで、並列性の奥深さを表現することができます。
ブレントの原理: ワーク w と深さ d の計算は、時間 O(max(w/p, d)) で p プロセッサ PRAM に実装できます。
並行性と並列性の両方とも、宣言型プログラミング、つまり不変性と RT も必要です。
では、Parallelism == Concurrency という危険な仮定はどこから来たのでしょうか? これは、副作用のある言語の自然な結果です。言語のいたるところに副作用がある場合、一度に複数のことを実行しようとすると、各操作の影響のインターリーブによって本質的に非決定性が生じます。 . したがって、副作用のある言語では、並列処理を実現する唯一の方法は並行性です。したがって、この 2 つが混同されていることがよくあることは驚くべきことではありません。
FP評価順序
評価順序は、機能構成の終了とパフォーマンスの副作用にも影響を与えることに注意してください。
Eager (CBV) と Lazy (CBN) は、評価順序が逆であるため、カテゴリカルな決闘です [ 10 ]。逆さまのツリーを想像してみてください。次に、関数ツリー ブランチの先端からブランチ階層を上ってトップレベルの関数トランクまで熱心に評価します。一方、レイジーは幹から枝の先端まで評価します。Eager には連言積 (「and」、別名カテゴリカル「積」) がなく、lazy には選言副積 (「or」、別名カテゴリカル「和」) がありません[ 11 ]。
パフォーマンス
- 熱心
非終了の場合と同様に、eager は結合機能合成であまりにも熱心です。つまり、合成制御構造は、lazy では行われない不必要な作業を行います。たとえば、最初の true 要素で終了する折り畳みで構成されている場合、リスト全体を熱心に不必要にブール値にマップします。
この不必要な作業は、どちらも純粋な関数を使用した、熱心な対怠惰な時間の連続的な複雑さの「最大」余分なlog n 係数を主張する原因です。解決策は、ファンクター (リストなど) をレイジー コンストラクター (つまり、オプションのレイジー プロダクトを使用するイーガー) と共に使用することです。これは、積が構成型、つまり初期固定点に初期代数を持つ帰納型であるためです [ 11 ]。
- 怠惰
非終了と同様に、lazy は選言的機能合成ではあまりにも怠惰です。つまり、coinductive finality が必要以上に遅く発生する可能性があり、その結果、不必要な作業と、eager[ 10 ][ 11 ]の場合とは異なる遅延の非決定性の両方が生じます。 . ファイナリティの例としては、状態、タイミング、非終了、実行時例外があります。これらは命令型の副作用ですが、純粋な宣言型言語 (Haskell など) でも、命令型 IO モナドに状態があり (注意: すべてのモナドが命令型というわけではありません!)、空間割り当てに暗示的であり、タイミングは命令型に相対的な状態です。現実の世界。オプションのeager coproductsでもlazyを使用すると、「laziness」が内部coproductsに漏れます。これは、lazyを使用すると、lazinessの誤りが外部関数に起因するためです(非終了セクションの例を参照してください。ここで == は外部二項演算子関数です)。これは、余積が最終性、つまり、最終オブジェクトの最終代数を伴う総帰納型によって制限されるためです [ 11 ]。
Lazy は、宣言された関数階層と実行時の評価順序との間に不協和があるため、遅延とスペースのために関数の設計とデバッグに不確定性を引き起こします。そのデバッグは、おそらく大多数のプログラマーの能力を超えています。熱心に評価された遅延純粋関数は、実行時に以前には見られなかった非終了を潜在的に導入する可能性があります。逆に、lazy で評価された熱心な純粋な関数は、実行時に以前には見えなかったスペースとレイテンシの不確定性を潜在的に導入する可能性があります。
非終了
コンパイル時には、ホルティング問題とチューリング完全言語の相互再帰により、通常、関数が終了することを保証できません。
- 熱心
積極的だが怠惰ではない場合、 Head
"and"の接続について、 orTail
のいずれHead
かが終了しない場合、左側は終了せず、右側は終了するため、Tail
それぞれ or のいずれList( Head(), Tail() ).tail == Tail()
かは真ではありません。List( Head(), Tail() ).head == Head()
一方、レイジーでは両側が終了します。このように、eager は連結積ではあまりにもeager であり、必要のない場合には終了しません (実行時例外を含む)。
- 怠惰
1
怠惰だが熱心ではない場合、 "or"の選言について、終了しない2
場合は、左側が終了し、右側が終了しないため、true ではありません。f
List( f ? 1 : 2, 3 ).tail == (f ? List( 1, 3 ) : List( 2, 3 )).tail
一方、熱心な場合はどちらの側も終了しないため、平等テストに到達することはありません。したがって、lazy は選言的な共積に対してはあまりにも怠惰であり、そのような場合、eager よりも多くの作業を行った後で (実行時例外を含めて) 終了できません。
[ 10 ] Declarative Continuations and Categorical Duality, Filinski, section 2.5.4 A comparison of CBV and CBN, and 3.6.1 CBV and CBN in the SCL.
[ 11 ] Declarative Continuations and Categorical Duality, Filinski, section 2.2.1 Products and coproducts, 2.2.2 Terminal and initial objects, 2.5.2 CBV with lazy products, and 2.5.3 CBN with ager coproducts.
これらについて、明確で客観的な定義はありません。これが私がそれらを定義する方法です:
必須- コンピューターが何をするかではなく、コンピューターが取るべき手順に焦点を当てます(例: C、C++、Java)。
宣言型- コンピューターがどのように実行するかではなく、何を実行するかに重点が置かれます (SQL など)。
関数型 - 再帰に重点を置いた宣言型言語のサブセット
命令型と宣言型は、プログラミングの 2 つの相反するスタイルを表しています。命令型は伝統的な「ステップバイステップのレシピ」アプローチであり、宣言型はより「これが私が望むものであり、今あなたはそれを行う方法を考え出す」というアプローチです。
これら 2 つのアプローチは、同じ言語と同じプログラムであっても、プログラミング全体で発生します。一般に、宣言型アプローチは、プログラマーが非常に多くの詳細を指定する必要がなく、バグの可能性が少ないため、好ましいと考えられています (必要な結果を記述し、十分にテストされた自動プロセスがそこから逆方向に動作して、ステップを定義しておけば、各ステップを手動で指定するよりも信頼性が高くなると期待できます)。
一方、命令型アプローチでは、より低レベルの制御が可能になります。これは、プログラミングに対する「マイクロマネージャー アプローチ」です。これにより、プログラマーは問題に関する知識を活用して、より効率的な回答を得ることができます。したがって、プログラムの一部がより宣言的なスタイルで記述されることは珍しくありませんが、速度が重要な部分はより命令的になります。
ご想像のとおり、プログラムを作成するために使用する言語は、宣言型になる方法に影響を与えます。結果の記述が与えられた場合に何をすべきかを理解するための組み込みの「スマート」を備えた言語は、はるかに宣言型にすることができます。より宣言的なレイヤーを上に構築できるようになる前に、プログラマーが最初にそのようなインテリジェンスを命令型コードで追加する必要があるアプローチよりも優れています。たとえば、prolog のような言語は、答えを検索するプロセスが組み込まれているため、非常に宣言的であると見なされます。
これまでのところ、関数型プログラミングについて触れていないことに気付くでしょう。これは、意味が他の 2 つに直接関係しない用語だからです。最も単純な関数型プログラミングは、関数を使用することを意味します。特に、「ファーストクラスの値」として関数をサポートする言語を使用すること - つまり、関数を記述できるだけでなく、関数を記述する関数 (関数を記述する関数) を記述し、関数をに渡すことができることを意味します。機能。要するに、その関数は、文字列や数値などと同じくらい柔軟で一般的です。
したがって、機能的、命令的、宣言的がしばしば一緒に言及されるのは奇妙に思えるかもしれません。この理由は、関数型プログラミングの考え方を「極限まで」取り入れた結果です。関数は、最も純粋な意味で、数学に由来するものです。つまり、何らかの入力を受け取り、常に同じ出力を返す一種の「ブラック ボックス」です。そのような動作では、変化する変数を保存する必要はありません。したがって、関数型プログラミングの非常に純粋で数学的に影響を受けたアイデアを実装することを目的とするプログラミング言語を設計すると、(特定の限定的な技術的な意味で) 変化する可能性のある値のアイデアを大部分拒否することになります。
そして、それを行うと、変数の変更方法を制限すると、ほぼ偶然に、プログラマーがより宣言的なプログラムを作成することを余儀なくされることになります。これは、命令型プログラミングの大部分が変数の変更方法を記述しているためです。それを行う!そのため、関数型プログラミング (特に関数型言語でのプログラミング) は、より宣言的なコードを提供する傾向があることがわかります。
要約すると、次のようになります。
命令型と宣言型は、プログラミングの 2 つの相反するスタイルです (これらのスタイルを奨励するプログラミング言語には同じ名前が使用されます)。
関数型プログラミングは、関数が非常に重要になり、結果として値の変更があまり重要でなくなるプログラミングのスタイルです。値の変更を指定する機能が限られているため、より宣言的なスタイルが強制されます。
そのため、「関数型プログラミング」は「宣言的」と表現されることがよくあります。
手短に:
命令型言語は、コンピューターが順番に実行する一連の命令を指定します (これを実行してから、あれを実行します) 。
宣言型言語は、どの出力がどの入力から生じるべきかに関する一連の規則を宣言します (たとえば、A がある場合、結果は B になります)。エンジンはこれらのルールを入力に適用し、出力を提供します。
関数型言語は、入力を出力に変換する方法を定義する一連の数学/論理関数を宣言します。例えば。f(y) = y * y. 宣言型言語の一種です。
命令型プログラミングとは、コンピューターによって実行される操作がどのように行われるかを説明する命令からプログラムが構成されている、あらゆるスタイルのプログラミングを意味します。
宣言型プログラミングとは、プログラムが問題または解決策のいずれかの記述である、あらゆるスタイルのプログラミングを意味しますが、作業がどのように行われるかを明示的には述べていません。
関数型プログラミングとは、関数と関数の関数を評価することによるプログラミングです...(厳密に定義された)関数型プログラミングとは、副作用のない数学関数を定義することによるプログラミングを意味するため、宣言型プログラミングの形式ですが、宣言型プログラミングの種類はこれだけではありません。。
論理プログラミング(たとえばProlog)は、宣言型プログラミングのもう1つの形式です。それは、論理ステートメントが真であるかどうか(またはそれが満たされることができるかどうか)を決定することによって計算することを含みます。プログラムは通常、一連の事実とルールです。つまり、一連の指示ではなく説明です。
項書き換え(CASLなど)は、宣言型プログラミングのもう1つの形式です。これには、代数用語の記号変換が含まれます。論理プログラミングや関数型プログラミングとは完全に異なります。
命令型-式は、実行する一連のアクションを記述します(連想)
宣言型-式は、プログラムの動作に寄与する宣言です(連想、可換、べき等、単調)
機能的-式は効果としてのみ価値があります。セマンティクスは等式推論をサポートします
今日、新しい焦点: 古い分類が必要ですか?
命令型/宣言型/関数型の側面は、過去にはジェネリック言語を分類するの に適していましたが、今日ではすべての「大きな言語」(Java、Python、Javascript など) には、「他の焦点」で表現するためのオプション (通常はフレームワーク) があります。その主要なもの(通常の命令)よりも、並列プロセス、宣言関数、ラムダなどを表現するために。
したがって、この質問の良い変形は、「今日のフレームワークを分類するのに適した側面は何ですか?」というものです。 ...重要な側面は、「プログラミングスタイル」とラベル付けできるものです...
データとアルゴリズムの融合に注目
説明する良い例。Wikipedia で jQuery について読むことができるように、
jQuery のコア機能のセット — DOM 要素の選択、トラバーサル、および操作 — は、そのセレクタ エンジン (...) によって有効になり、アルゴリズムと DOM データ構造を融合する新しい「プログラミング スタイル」を作成しました。
したがって、jQuery は、オブジェクト指向だけでなく、「アルゴリズムとデータ構造の融合」である「新しいプログラミング スタイル」に焦点を当てた最良の (人気のある) 例です。jQuery は、スプレッドシートとして多少反応的ですが、「セル指向」ではなく、「DOM ノード指向」です...このコンテキストでの主なスタイルの比較:
融合なし: すべての「ビッグ言語」、関数型/宣言型/命令型の表現では、厳密な代数構造の観点での融合であるオブジェクト指向を除いて、通常はデータとアルゴリズムの「融合なし」です。
いくつかの融合: 融合のすべての古典的な戦略は、今日ではそれをパラダイムとして使用するフレームワークを持っています... データフロー、 イベント駆動型プログラミング(またはawkやXSLTなどの古いドメイン固有言語)... 最新のスプレッドシートを使用したプログラミングのように、それらもリアクティブ プログラミングスタイルの例。
大きな融合: 「jQuery スタイル」です... jQuery は、「アルゴリズムとDOM データ構造の融合」に焦点を当てたドメイン固有言語です。 PS: XQuery、SQL (PL を命令式オプションとして使用) などの他の「クエリ言語」もデータ アルゴリズム フュージョンの例ですが、他のシステム モジュールとのフュージョンがない島です... Spring、 -variantsを使用する場合および仕様条項は、もう 1 つの優れた融合の例です。
find()
宣言型プログラミングは、入力と出力の間の時代を超越したロジックを表現することによるプログラミングです。たとえば、疑似コードで、次の例は宣言型になります。
def factorial(n):
if n < 2:
return 1
else:
return factorial(n-1)
output = factorial(argvec[0])
ここで「階乗」と呼ばれる関係を定義し、出力と入力の関係をその関係として定義しただけです。ここで明らかなように、どの構造化言語でも宣言型プログラミングがある程度まで可能です。宣言型プログラミングの中心的な考え方は、不変データです。変数に代入する場合、代入は 1 回だけで、その後は二度とできません。他のより厳密な定義では、副作用がまったくない可能性があり、これらの言語は「純粋に宣言的」と呼ばれることがあります。
命令型スタイルでの同じ結果は次のようになります。
a = 1
b = argvec[0]
while(b < 2):
a * b--
output = a
この例では、入力と出力の間の時代を超越した静的な論理関係を表現していません。いずれかが目的の結果を保持するまで、手動でメモリ アドレスを変更しました。すべての言語が宣言的セマンティクスをある程度拡張できることは明らかですが、すべてが命令型を許可しているわけではなく、一部の「純粋な」宣言型言語は副作用と突然変異を完全に許可しています。
宣言型言語は、「どのように行うか」ではなく、「何をしなければならないか」を指定するとよく言われますが、これは誤称だと思います。宣言型プログラムは、入力から出力までを取得する方法を指定しますが、別の言い方をすれば、指定する関係は、効果的に計算可能でなければなりません(重要な用語です。わからない場合は調べてください)。別のアプローチは、非決定論的プログラミングです。これは、実装が成功するまで試行錯誤ですべてのパスを使い果たす前に、結果がどの条件を満たしているかを実際に指定するだけです。
純粋な宣言型言語には、Haskell と Pure Prolog が含まれます。一方から他方へのスライディング スケールは次のようになります: Pure Prolog、Haskell、OCaml、Scheme/Lisp、Python、Javascript、C--、Perl、PHP、C++、Pascall、C、Fortran、Assembly
一言で言えば、プログラミング スタイルが何を (何をする) かを強調し、どのように (何をするか) の詳細を抽象化するほど、そのスタイルは宣言的であると見なされます。命令の場合はその逆です。関数型プログラミングは、宣言型スタイルに関連付けられています。