プロシージャルと機能的プログラミングパラダイムの違いを理解するのは本当に大変です。
機能プログラミングに関するWikipediaエントリの最初の2つの段落を次に示します。
コンピューターサイエンスでは、関数型プログラミングは、計算を数学関数の評価として扱い、状態および可変データを回避するプログラミングパラダイムです。状態の変化を強調する命令型プログラミングスタイルとは対照的に、関数の適用を強調します。関数型プログラミングは、1930年代に関数定義、関数アプリケーション、再帰を調査するために開発された正式なシステムであるラムダ計算に根ざしています。多くの関数型プログラミング言語は、ラムダ計算の詳細と見なすことができます。
実際には、数学関数と命令型プログラミングで使用される「関数」の概念の違いは、命令型関数が副作用を持ち、プログラムの状態の値を変える可能性があるということです。このため、参照の透明性に欠けています。つまり、同じ言語式は、実行中のプログラムの状態に応じて、異なる時間に異なる値になる可能性があります。逆に、機能コードでは、関数の出力値は関数に入力される引数のみに依存するため、引数
f
に同じ値を指定して関数x
を2回呼び出すと、同じ結果f(x)
両方の時間。副作用をなくすことで、プログラムの動作をより簡単に理解および予測できるようになります。これは、関数型プログラミングの開発の重要な動機の1つです。
段落2では
逆に、機能コードでは、関数の出力値は関数に入力される引数のみに依存するため、引数
f
に同じ値を指定して関数x
を2回呼び出すと、同じ結果f(x)
両方の時間。
手続き型プログラミングの場合とまったく同じではありませんか?
手続き型と機能型で目立つものは何ですか?
関数型プログラミング
関数型プログラミングとは、関数を値として扱う能力のことです。
「通常の」値との類推を考えてみましょう。 2つの整数値を取り、それらを_+
_演算子を使用して結合して、新しい整数を取得できます。または、整数に浮動小数点数を乗算して、浮動小数点数を取得できます。
関数型プログラミングでは、 compose や lift などの演算子を使用して、2つの関数値を組み合わせて新しい関数値を生成できます。または、関数値とデータ値を組み合わせて、 map や fold などの演算子を使用して新しいデータ値を生成できます。
多くの言語には関数型プログラミング機能があり、通常は関数型言語とは考えられていない言語も含まれます。 Grandfather FORTRANでさえ関数値をサポートしていましたが、関数を結合する演算子の方法はあまり提供していませんでした。 「関数型」と呼ばれる言語では、関数型プログラミング機能を大いに取り入れる必要があります。
手続き型プログラミング
手続き型プログラミングとは、一般的な一連の命令をプロシージャにカプセル化して、コピーアンドペーストに頼らずに多くの場所からそれらの命令を呼び出すことができる機能のことです。プロシージャはプログラミングのごく初期の開発であったため、この機能は、ほとんどの場合、機械言語またはアセンブリ言語のプログラミングで要求されるプログラミングスタイルと関連付けられています。スタイルは、ストレージロケーションの概念とそれらのロケーション間でデータを移動する命令を強調するスタイルです。
コントラスト
2つのスタイルは実際には正反対ではありません-それらは互いに異なるだけです。両方のスタイルを完全に包含する言語があります(たとえば、LISP)。次のシナリオは、2つのスタイルのいくつかの違いの感覚を与えるかもしれません。リスト内のすべての単語の文字数が奇数であるかどうかを判断するナンセンス要件のコードを作成してみましょう。まず、手続き型:
_function allOdd(words) {
var result = true;
for (var i = 0; i < length(words); ++i) {
var len = length(words[i]);
if (!odd(len)) {
result = false;
break;
}
}
return result;
}
_
この例は理解できると思います。今、機能的なスタイル:
_function allOdd(words) {
return apply(and, map(compose(odd, length), words));
}
_
この定義では、徹底的に次のことを行います。
compose(odd, length)
は、odd
関数とlength
関数を組み合わせて、文字列の長さが奇数であるかどうかを判断する新しい関数を生成します。map(..., words)
は、words
の各要素に対して新しい関数を呼び出し、最終的にブール値の新しいリストを返します。各リストは、対応するWordの文字数が奇数かどうかを示します。apply(and, ...)
は、結果リストに「and」演算子を適用し、and-すべてのブール値をまとめて最終結果を生成します。これらの例からわかるように、手続き型プログラミングは、変数内で値を移動し、最終結果を生成するために必要な操作を明示的に記述することに非常に関係しています。対照的に、機能スタイルは、初期入力を最終出力に変換するために必要な機能の組み合わせを強調しています。
この例では、手続き型コードと機能コードの一般的な相対サイズも示しています。さらに、手続き型コードのパフォーマンス特性は、機能コードのパフォーマンス特性よりも見やすいかもしれないことを示しています。考慮:関数はリスト内のすべての単語の長さを計算しますか、それとも最初の偶数長の単語を見つけた直後にそれぞれ停止しますか?一方、関数型コードは、明示的なアルゴリズムではなく主に意図を表現するため、高品質の実装でかなり深刻な最適化を実行できます。
さらに読む
この質問はたくさん出てきます...例えば、以下をご覧ください:
ジョン・バッカスのチューリング賞の講義では、関数型プログラミングの動機について詳細に説明しています。
この論文は、かなり技術的で、かなり早くなるので、現在の文脈では本当に言及すべきではありません。本当に基本的なことだと思うので、抵抗できませんでした。
補遺-2013
コメンテーターは、人気のある現代言語が手続き型および関数型に加えて他のスタイルのプログラミングを提供することを指摘しています。このような言語は、多くの場合、次のプログラミングスタイルの1つ以上を提供します。
このレスポンスの擬似コードの例が、他のスタイルで利用可能な機能のいくつかからどのように利益を得ることができるかの例については、以下のコメントを参照してください。特に、手続き型の例は、事実上すべての高レベルの構造の適用から恩恵を受けます。
展示されている例では、議論中の2つのスタイルの違いを強調するために、これらの他のプログラミングスタイルの混合を意図的に避けています。
関数型プログラミングと命令型プログラミングの本当の違いは考え方です-命令型プログラマーは変数とメモリのブロックを考えていますが、関数型プログラマーは「どうすれば変換入力データを出力データに」と考えています- 「プログラム」はパイプラインであり、dataの変換セットで、入力から出力に変換します。これはIMOの興味深い部分であり、「変数を使用しないでください」ビットではありません。
この考え方の結果として、FPプログラムは通常、whatが、howそれが起こる-これは強力です。なぜなら、「選択」と「場所」と「集約」の意味を明確に述べることができれば、自由に交換できるからです。 AsParallel()で行うように、その実装は突然、シングルスレッドアプリケーションがnコアにスケールアウトします。
Isn't that the same exact case for procedural programming?
いいえ、手続き型コードには副作用があるためです。たとえば、呼び出し間の状態を保存できます。
とはいえ、手続き型と見なされる言語でこの制約を満たすコードを書くことは可能です。また、機能的と見なされる一部の言語でこの制約を破るコードを記述することもできます。
WReachの答えには同意しません。不一致の原因を確認するために、彼の答えを少し分解してみましょう。
まず、彼のコード:
function allOdd(words) {
var result = true;
for (var i = 0; i < length(words); ++i) {
var len = length(words[i]);
if (!odd(len)) {
result = false;
break;
}
}
return result;
}
そして
function allOdd(words) {
return apply(and, map(compose(odd, length), words));
}
最初に注意することは、彼が混同していることです。
プログラミング、および典型的な機能スタイルよりも明示的な制御フローを持つ反復スタイルプログラミングの機能が欠落しています。
これらについてすぐに話しましょう。
表現中心のスタイルとは、可能な限り物事を評価するものです。関数型言語は表現を好むことで有名ですが、実際には、構成可能な表現のない関数型言語を持つことは可能です。 no式、単なるステートメントが存在する場合、1つを作成します。
lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and
これは、関数がステートメントとバインディングのチェーンを介して純粋にチェーンされることを除いて、前に示したものとほとんど同じです。
イテレーター中心のプログラミングスタイルは、Pythonで採用されているスタイルかもしれません。 purely反復、反復子中心のスタイルを使用してみましょう。
def all_odd(words):
lengths = (len(Word) for Word in words)
each_odd = (odd(length) for length in lengths)
return all(each_odd)
各句は反復プロセスであり、スタックフレームの明示的な一時停止と再開によって結合されるため、これは機能しません。構文は部分的に関数型言語からインスピレーションを受けている場合がありますが、完全に反復的な実施形態に適用されます。
もちろん、これを圧縮できます:
def all_odd(words):
return all(odd(len(Word)) for Word in words)
命令型は今ではそれほど悪くはないようですね。 :)
最後のポイントは、より明示的な制御フローについてでした。これを利用するために元のコードを書き直しましょう:
function allOdd(words) {
for (var i = 0; i < length(words); ++i) {
if (!odd(length(words[i]))) {
return false;
}
}
return true;
}
イテレータを使用すると、次のことができます。
function allOdd(words) {
for (Word : words) { if (!odd(length(Word))) { return false; } }
return true;
}
違いが次の場合の機能言語のポイントis
return all(odd(len(Word)) for Word in words)
return apply(and, map(compose(odd, length), words))
for (Word : words) { if (!odd(length(Word))) { return false; } }
return true;
関数型プログラミング言語の主な決定的な特徴は、典型的なプログラミングモデルの一部として突然変異を取り除くことです。多くの場合、これは関数型プログラミング言語にステートメントがなく、式を使用しないことを意味しますが、これは単純化です。関数型言語は、明示的な計算を振る舞いの宣言で置き換えます。その後、言語はそれを縮小します。
この機能のサブセットに制限することにより、プログラムの動作についてより多くの保証を得ることができ、これにより、プログラムをより自由に構成できます。
関数型言語を使用している場合、新しい関数の作成は通常、密接に関連する関数を作成するのと同じくらい簡単です。
all = partial(apply, and)
関数のグローバルな依存関係を明示的に制御していない場合、これは単純ではなく、おそらく不可能です。関数型プログラミングの最大の特徴は、より一般的な抽象化を一貫して作成し、それらをより大きな全体に結合できることを信頼できることです。
手続き型のパラダイム(代わりに「構造化プログラミング」と言いますか?)では、可変メモリと、それを何らかの順序で(次々に)読み書きする命令を共有しています。
関数型パラダイムでは、変数と関数があります(数学的な意味で:変数は時間とともに変化せず、関数は入力に基づいて何かを計算することしかできません)。
(これは単純化されすぎており、例えば、FPLには通常、可変メモリを操作する機能がありますが、手続き型言語は高次の手続きをサポートすることが多いため、物事はそれほど明確ではありませんが、これはあなたにアイデアを与えるはずです)
魅力的なPython:Pythonでの関数型プログラミング from IBM Developerworks は、違いを理解するのに本当に役立ちました。
特にPythonを少し知っている人にとっては、機能と手続きの異なることを行うこの記事のコード例は、手続き型プログラミングと関数型プログラミングの違いを明確にすることができます。
関数プログラミングでは、シンボルの意味(変数名または関数名)を推論するために、実際に必要なことは2つだけです(現在のスコープとシンボルの名前)。不変の純粋に機能的な言語を持っている場合、これらは両方とも「静的」(ひどくオーバーロードされた名前の申し訳ありません)の概念です。つまり、ソースコードを見るだけで、現在のスコープと名前の両方を見ることができます。
手続き型プログラミングでは、x
の背後にある値は何であるかという質問に答えたい場合、どのようにそこに到達したかを知る必要もあります。スコープと名前だけでは十分ではありません。この実行パスは「ランタイム」プロパティであり、非常に多くの異なるものに依存する可能性があるため、これは私が最大の課題と見なすものです。ほとんどの人は、実行パスを試行して回復するのではなく、デバッグすることを学びます。
私は最近、 Expression Problem の点で違いを考えています。 Phil Wadlerの説明 はよく引用されますが、 この質問 への受け入れられた答えはおそらくたぶん簡単です。基本的に、命令型言語は問題に対する1つのアプローチを選択する傾向があり、関数型言語は他のアプローチを選択する傾向があります。