手続き型プログラミングと関数型プログラミングの違いは何ですか?[閉まっている]
-
09-06-2019 - |
質問
両方のウィキペディアの記事を読みました 手続き型プログラミング そして 関数型プログラミング, しかし、私はまだ少し混乱しています。誰かがそれを核心まで煮詰めてくれませんか?
解決
関数型言語を使用すると、(理想的には) 数学関数を作成できます。を取る関数 n 引数を受け取り、値を返します。プログラムが実行されると、この関数は必要に応じて論理的に評価されます。1
一方、手続き型言語は一連の処理を実行します。 一連 ステップ。(順序ロジックを関数ロジックに変換する方法があります。 継続パススタイル.)
結果として、純粋に関数型のプログラムは常に次の結果をもたらします。 同じ値 入力の場合、評価の順序は明確に定義されていません。これは、ユーザー入力やランダムな値などの不確実な値を純粋な関数型言語でモデル化するのが難しいことを意味します。
1 この回答の他のすべてと同様、これは一般化です。呼び出された場所で順番に計算を評価するのではなく、結果が必要になったときに計算を評価する特性は「遅延」として知られています。実際にすべての関数型言語が普遍的に遅延するわけではありませんし、遅延は関数型プログラミングに限定されるわけでもありません。むしろ、ここでの説明は、明確で対立するカテゴリーではなく、むしろ流動的なアイデアであるさまざまなプログラミング スタイルについて考えるための「精神的な枠組み」を提供します。
他のヒント
基本的に、この 2 つのスタイルは陰と陽のようなものです。1 つは組織化されており、もう 1 つは混沌としています。関数型プログラミングが当然の選択である状況もあれば、手続き型プログラミングの方が適切な選択である状況もあります。これが、両方のプログラミング スタイルを採用した新しいバージョンが最近リリースされた言語が少なくとも 2 つある理由です。 ( Perl6 そして D 2 )
手順:
- ルーチンの出力は、必ずしも入力と直接の相関関係があるとは限りません。
- すべては特定の順序で行われます。
- ルーチンの実行には副作用が生じる可能性があります。
- 直線的な方法でソリューションを実装することを重視する傾向があります。
パール6
sub factorial ( UInt:D $n is copy ) returns UInt {
# modify "outside" state
state $call-count++;
# in this case it is rather pointless as
# it can't even be accessed from outside
my $result = 1;
loop ( ; $n > 0 ; $n-- ){
$result *= $n;
}
return $result;
}
D2
int factorial( int n ){
int result = 1;
for( ; n > 0 ; n-- ){
result *= n;
}
return result;
}
機能:
- 多くの場合、再帰的です。
- 指定された入力に対して常に同じ出力を返します。
- 評価の順序は通常未定義です。
- 無国籍である必要があります。つまりどの操作にも副作用が生じることはありません。
- 並列実行に最適
- 分割統治のアプローチを強調する傾向があります。
- 遅延評価の機能がある場合があります。
ハスケル
(からコピー ウィキペディア );
fac :: Integer -> Integer
fac 0 = 1
fac n | n > 0 = n * fac (n-1)
または 1 行で:
fac n = if n > 0 then n * fac (n-1) else 1
パール6
proto sub factorial ( UInt:D $n ) returns UInt {*}
multi sub factorial ( 0 ) { 1 }
multi sub factorial ( $n ) { $n * samewith $n-1 } # { $n * factorial $n-1 }
D2
pure int factorial( invariant int n ){
if( n <= 1 ){
return 1;
}else{
return n * factorial( n-1 );
}
}
サイドノート:
Factorial は、実際には、サブルーチンを作成するのと同じ方法で Perl 6 で新しい演算子を作成することがいかに簡単かを示す一般的な例です。この機能は Perl 6 に組み込まれているため、Rakudo 実装のほとんどの演算子はこの方法で定義されています。また、既存の演算子に独自の複数の候補を追加することもできます。
sub postfix:< ! > ( UInt:D $n --> UInt )
is tighter(&infix:<*>)
{ [*] 2 .. $n }
say 5!; # 120
この例では、範囲の作成も示しています (2..$n
) とリスト削減メタ演算子 ([ OPERATOR ] LIST
) 数値中置乗算演算子と組み合わせます。(*
)
また、置くことができることも示しています --> UInt
代わりに署名に returns UInt
その後。
(範囲を開始することで回避できます 2
乗算「演算子」が返されるため、 1
引数なしで呼び出された場合)
この定義が他で与えられているのを見たことがありませんが、これはここで与えられた違いをかなりよく要約していると思います。
機能的 プログラミングは以下に焦点を当てます 表現
手続き的 プログラミングは以下に焦点を当てます ステートメント
式には値があります。関数型プログラムは、コンピュータが実行する一連の命令を値とする式です。
ステートメントには値はなく、代わりに何らかの概念的なマシンの状態を変更します。
純粋な関数型言語では、状態を操作する方法がないという意味で、ステートメントは存在しません (「ステートメント」という名前の構文構造がまだある可能性がありますが、状態を操作しない限り、この意味ではステートメントとは呼びません) )。純粋な手続き型言語では式は存在せず、すべてがマシンの状態を操作する命令になります。
Haskell は、状態を操作する方法がないため、純粋な関数型言語の例になります。プログラム内のすべてがマシンのレジスターとメモリーの状態を操作するステートメントであるため、マシンコードは純粋な手続き型言語の一例になります。
混乱を招くのは、ほとんどのプログラミング言語には次のものが含まれていることです。 両方 式とステートメントを使用して、パラダイムを混合できます。言語は、式とステートメントの使用をどの程度奨励しているかに基づいて、より機能的であるか、より手続き的であると分類できます。
たとえば、関数呼び出しは式であるのに対し、COBOL ではサブプログラムの呼び出しはステートメント (共有変数の状態を操作し、値を返さない) であるため、C は COBOL よりも機能的です。Python は、条件付きロジックを短絡評価 (if ステートメントではなく test && path1 || path2) を使用した式として表現できるため、C よりも機能的です。Scheme 内のすべてが式であるため、Scheme は Python よりも機能的です。
手続き型パラダイムを奨励する言語でも関数型スタイルで記述することはできますし、その逆も同様です。言語によって推奨されていないパラダイムで書くのは、より難しく、よりぎこちないだけです。
コンピューター サイエンスにおける関数型プログラミングは、計算を数学関数の評価として扱い、状態データや可変データを回避するプログラミング パラダイムです。状態の変化を重視する手続き型プログラミング スタイルとは対照的に、関数の適用を重視します。
手続き型/関数型/目的型プログラミングとは、問題にどうアプローチするかということだと思います。
最初のスタイルでは、すべてをステップに分けて計画し、一度に 1 つのステップ (手順) を実装することで問題を解決します。一方、関数型プログラミングでは分割統治アプローチが強調されます。つまり、問題がサブ問題に分割され、各サブ問題が解決され (そのサブ問題を解決する関数を作成し)、その結果が結合されます。問題全体に対する答えを作成します。最後に、客観的プログラミングは、それぞれが (ある程度) 独自の特性を持ち、他のオブジェクトと相互作用する多くのオブジェクトを含むミニ世界をコンピューター内に作成することによって、現実世界を模倣します。それらの相互作用から結果が生まれます。
プログラミングの各スタイルには、独自の長所と短所があります。したがって、「純粋なプログラミング」のようなことを行うことになります(つまり、純粋に手続き型(ところで、誰もこれをやらないのですが、それは一種の奇妙です)は、プログラミングスタイルの利点を実証するために特別に設計されたいくつかの初歩的な問題を除いて、不可能ではないにしても非常に困難です(したがって、私たちは純粋さを好む人を「ウィニー」と呼びます :D)。
そして、それらのスタイルから、それぞれのスタイルに最適化されるように設計されたプログラミング言語があります。たとえば、アセンブリはすべて手順に基づいています。C や Pascal などの Asm だけでなく、初期の言語のほとんどは手続き型です (そして Fortran もそうです)。そして、客観的な学校では有名な Java がすべて揃っています (実際には、Java と C# も「お金重視」と呼ばれるクラスに属しますが、それについては別の議論の対象となります)。Smalltalk も目的です。関数型学校では、「ほぼ関数型」(不純であると考える人もいます) Lisp ファミリーと ML ファミリー、そして多くの「純粋関数型」 Haskell、Erlang などが存在します。ちなみに一般言語はPerl、Python、Rubyなど多数あります。
Konrad のコメントをさらに詳しく説明すると、次のようになります。
結果として、純粋に関数型のプログラムは入力に対して常に同じ値を生成し、評価の順序は明確に定義されていません。
このため、関数型コードは一般に並列化が容易です。関数には (通常) 副作用がなく、(通常) 引数に基づいて動作するだけであるため、多くの同時実行性の問題は解決されます。
関数型プログラミングは、次の能力が必要な場合にも使用されます。 証明する あなたのコードは正しいです。これを手続き型プログラミングで行うのははるかに困難です (関数型プログラミングでは簡単ではありませんが、それでも簡単です)。
免責事項:私はもう何年も関数型プログラミングを使用しておらず、最近になって関数型プログラミングを再び検討し始めたばかりなので、ここでの私が完全に正しいわけではないかもしれません。:)
私がここで特に強調されていなかったのは、Haskell などの最新の関数型言語は、実際には明示的な再帰よりもフロー制御のためのファーストクラス関数に重点を置いているということです。上記のように、Haskell で階乗を再帰的に定義する必要はありません。私は次のようなことを思います
fac n = foldr (*) 1 [1..n]
これは完全に慣用的な構造であり、明示的な再帰を使用するよりもループを使用することに精神的に近いです。
関数型プログラミングは、グローバル変数が次のような手続き型プログラミングと同じです。 ない 使用されています。
手続き型言語は (変数を使用して) 状態を追跡し、一連のステップとして実行する傾向があります。純粋な関数型言語は状態を追跡せず、不変の値を使用し、一連の依存関係として実行される傾向があります。多くの場合、コール スタックのステータスには、手続き型コードの状態変数に格納される情報と同等の情報が保持されます。
再帰は関数型プログラミングの典型的な例です。
コンラッド氏はこう語った。
結果として、純粋に機能的なプログラムは常に入力に対して同じ値をもたらし、評価の順序は十分に定義されていません。つまり、ユーザー入力やランダム値などの不確実な値は、純粋に機能的な言語でモデル化するのが難しいことを意味します。
純粋に関数型のプログラムにおける評価の順序は、(特に怠惰な場合には)推論するのが難しいか、重要でさえないかもしれませんが、それが明確に定義されていないと言うと、プログラムが実行されているかどうかがわからないように聞こえると思います全然働くために!
おそらく、関数型プログラムの制御フローは、関数の引数の値がいつ必要になるかに基づいている、という方が適切な説明になるでしょう。これの良い点は、よく書かれたプログラムでは状態が明示的になることです。各関数は入力を任意にリストするのではなくパラメータとしてリストします。 むしゃむしゃ 世界的な状態。したがって、あるレベルでは、 一度に 1 つの関数について評価の順序を推論する方が簡単です. 。各関数は、残りの部分を無視して、実行する必要があることに集中できます。組み合わせた場合、関数は単独で動作する場合と同様に動作することが保証されます[1]。
...ユーザー入力やランダム値などの不確実な値は、純粋に機能的な言語でモデル化するのが難しいです。
純粋関数型プログラムにおける入力の問題の解決策は、命令型言語を DSL を使用して 十分に強力な抽象化. 。命令型 (または非純粋関数型) 言語では、「チート」して暗黙的に状態を渡すことができ、評価順序は (好むと好まざるにかかわらず) 明示的であるため、これは必要ありません。この「不正行為」とすべての関数のすべてのパラメーターの強制評価により、命令型言語では、1) 独自の制御フロー メカニズム (マクロなし) を作成できなくなります。2) コードは本質的にスレッド セーフではなく、並列化可能でもありません。 デフォルトでは, 3) 元に戻す (タイムトラベル) のようなものを実装するには慎重な作業が必要です (命令型プログラマは、古い値を戻すためのレシピを保存しなければなりません!) が、純粋な関数型プログラミングでは、これらすべてのものを手に入れることができます。忘れていた――「無料で」。
これが熱狂的であるように聞こえないことを願っていますが、私はただいくつかの視点を追加したかっただけです。C# 3.0 のような強力な言語での命令型プログラミング、特に混合パラダイム プログラミングは、依然として物事を成し遂げるための完全に効果的な方法です。 特効薬はない.
[1] ...ただし、おそらくメモリ使用量に関しては例外です (cf.Haskellのfoldlとfoldl')。
Konrad のコメントをさらに詳しく説明すると、次のようになります。
そして、評価の順序は明確に定義されていません
一部の関数型言語には遅延評価と呼ばれるものがあります。つまり、値が必要になるまで関数は実行されません。それまでは、関数自体が受け渡されるものです。
手続き型言語はステップ 1、ステップ 2、ステップ 3...ステップ 2 で「add 2 + 2」と言えば、その場で正しく実行されます。遅延評価では、2 + 2 を加算すると言いますが、結果がまったく使用されない場合、加算は行われません。
機会があれば、Lisp/Scheme のコピーを入手して、その中でいくつかのプロジェクトを実行することをお勧めします。最近流行りのアイデアのほとんどは、数十年前に Lisp で表現されました。関数型プログラミング、継続 (クロージャとして)、ガベージ コレクション、さらには XML。
したがって、これは、これらすべての現在のアイデアや、シンボリック計算などの他のいくつかのアイデアを有利にスタートするための良い方法となるでしょう。
関数型プログラミングが何に適しており、何に適していないのかを知っておく必要があります。すべてにおいて良いわけではありません。問題によっては、副作用という言葉で表現するのが最も適切であり、同じ質問でも、尋ねられるタイミングによって異なる答えが得られます。
@クレイトン:
Haskellには、というライブラリ関数があります。 製品:
prouduct list = foldr 1 (*) list
または単に:
product = foldr 1 (*)
したがって、「慣用的な」階乗
fac n = foldr 1 (*) [1..n]
単にそうなるだろう
fac n = product [1..n]
関数プログラミング
num = 1
def function_to_add_one(num):
num += 1
return num
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
#Final Output: 2
手続き型プログラミング
num = 1
def procedure_to_add_one():
global num
num += 1
return num
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
#Final Output: 6
function_to_add_one
関数です
procedure_to_add_one
手続きです
を実行しても、 関数 5回、そのたびに戻ってきます 2
を実行すると、 手順 5 回実行すると、5 回目の実行の終わりに次の結果が得られます。 6.
手続き型プログラミングでは、一連のステートメントと条件構成を、(非機能的な) 値である引数に対してパラメーター化されたプロシージャと呼ばれる個別のブロックに分割します。
関数プログラミングは、関数がファーストクラスの値であることを除いて同じです。そのため、関数を引数として他の関数に渡したり、関数呼び出しの結果として返すことができます。
この解釈では、関数型プログラミングは手続き型プログラミングを一般化したものであることに注意してください。しかし、少数派は「関数型プログラミング」を副作用のないことを意味すると解釈していますが、これはまったく異なりますが、Haskell を除くすべての主要な関数型言語にとっては無関係です。
違いを理解するには、手続き型プログラミングと関数型プログラミングの両方の「ゴッドファーザー」パラダイムが、 命令型プログラミング.
基本的に、手続き上のプログラミングは、抽象化の主な方法が「手順」である命令プログラムを構築する方法にすぎません。 (または一部のプログラミング言語で「機能」)。オブジェクト指向プログラミングでさえ、命令型プログラムを構築するもう 1 つの方法にすぎません。状態はオブジェクトにカプセル化され、「現在の状態」を持つオブジェクトになります。さらに、このオブジェクトには一連の関数、メソッド、その他の要素が含まれており、プログラマは状態を操作または更新します。
さて、関数型プログラミングに関して言えば、 要旨 そのアプローチは、どのような値を取得し、それらの値をどのように転送するかを特定することです。(つまり、関数をファーストクラスの値として受け取り、それらをパラメータとして他の関数に渡すため、状態や変更可能なデータは存在しません)。
追伸:あらゆるプログラミング パラダイムが使用されることを理解することで、それらすべての違いが明確になるはずです。
PSS:結局のところ、プログラミング パラダイムは問題を解決するためのさまざまなアプローチにすぎません。
PSS: これ Quoraの回答には素晴らしい説明があります。
ここでの回答は、慣用的な関数型プログラミングを示したものではありません。再帰的階乗の答えは FP での再帰を表すのに最適ですが、コードの大部分は再帰的ではないため、その答えが完全に代表的であるとは思えません。
文字列の配列があり、各文字列が「5」や「-200」などの整数を表すとします。この入力文字列配列を内部テスト ケースと照合してチェックしたいとします (整数比較を使用)。両方の解決策を以下に示します
手続き的
arr_equal(a : [Int], b : [Str]) -> Bool {
if(a.len != b.len) {
return false;
}
bool ret = true;
for( int i = 0; i < a.len /* Optimized with && ret*/; i++ ) {
int a_int = a[i];
int b_int = parseInt(b[i]);
ret &= a_int == b_int;
}
return ret;
}
機能的
eq = i, j => i == j # This is usually a built-in
toInt = i => parseInt(i) # Of course, parseInt === toInt here, but this is for visualization
arr_equal(a : [Int], b : [Str]) -> Bool =
zip(a, b.map(toInt)) # Combines into [Int, Int]
.map(eq)
.reduce(true, (i, j) => i && j) # Start with true, and continuously && it with each value
純粋な関数型言語は一般に研究用言語ですが (現実世界は自由な副作用を好むため)、現実世界の手続き型言語は、必要に応じて、より単純な関数構文を使用します。
これは通常、次のような外部ライブラリを使用して実装されます。 ロダッシュ, 、または次のような新しい言語で組み込みで利用可能 さび. 。関数型プログラミングの重労働は、次のような関数/概念を使用して行われます。 map
, filter
, reduce
, currying
, partial
, 最後の 3 つは、さらに理解するために参照できます。
補遺
関数呼び出しのオーバーヘッドが高すぎるため、実際に使用するには、コンパイラは通常、関数型バージョンを内部で手続き型バージョンに変換する方法を考案する必要があります。示されている階乗などの再帰的なケースでは、次のようなトリックが使用されます。 テールコール O(n) 個のメモリ使用量を削除します。副作用がないという事実により、関数型コンパイラーは && ret
たとえ .reduce
最後に行われます。JS で Lodash を使用すると、明らかに最適化ができないため、パフォーマンスが低下します (Web 開発では通常、これは問題になりません)。Rust のような言語は内部的に最適化します (そして次のような機能があります) try_fold
手伝う && ret
最適化)。