手続き型プログラミングと関数型プログラミングの両方についてウィキペディアの記事を読みましたが、それでも少し混乱しています。誰かがそれを核心まで煮詰めることができますか?
17 に答える
関数型言語(理想的には)を使用すると、数学関数、つまりn個の引数を取り値を返す関数を記述できます。プログラムが実行されると、この関数は必要に応じて論理的に評価されます。1
一方、手続き型言語は、一連の連続したステップを実行します。(シーケンシャルロジックを継続渡しスタイルと呼ばれる関数型ロジックに変換する方法があります。)
結果として、純粋に関数型のプログラムは常に入力に対して同じ値を生成し、評価の順序は明確に定義されていません。つまり、ユーザー入力やランダム値などの不確実な値は、純粋に関数型言語でモデル化するのは難しいということです。
1この回答の他のすべてと同様に、それは一般化です。このプロパティは、呼び出された場所ではなく、結果が必要なときに計算を評価するもので、「怠惰」と呼ばれます。すべての関数型言語が実際に普遍的に怠惰であるわけではなく、怠惰が関数型プログラミングに制限されているわけでもありません。むしろ、ここでの説明は、明確で反対のカテゴリではなく、流動的なアイデアであるさまざまなプログラミングスタイルについて考えるための「メンタルフレームワーク」を提供します。
基本的に2つのスタイルは、陰と陽のようなものです。1つは整理されており、もう1つは混沌としている。関数型プログラミングが明白な選択である状況もあれば、手続き型プログラミングがより良い選択である状況もあります。これが、最近、両方のプログラミングスタイルを採用した新しいバージョンがリリースされた言語が少なくとも2つある理由です。(Perl6およびD2)
#手順:#
- ルーチンの出力は、必ずしも入力と直接的な相関関係があるとは限りません。
- すべてが特定の順序で行われます。
- ルーチンの実行には副作用がある場合があります。
- ソリューションを直線的に実装することを強調する傾向があります。
## Perl 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;
}
## D 2 ##
int factorial( int n ){
int result = 1;
for( ; n > 0 ; n-- ){
result *= n;
}
return result;
}
#機能:#
- 多くの場合、再帰的です。
- 指定された入力に対して常に同じ出力を返します。
- 評価の順序は通常未定義です。
- ステートレスである必要があります。つまり、どの操作も副作用を引き起こす可能性はありません。
- 並列実行に最適
- 分割統治法を強調する傾向があります。
- 遅延評価の機能がある場合があります。
## Haskell ##(ウィキペディアからコピー);
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
## Perl 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 }
## D 2 ##
pure int factorial( invariant int n ){
if( n <= 1 ){
return 1;
}else{
return n * factorial( n-1 );
}
}
#サイドノート:#
階乗は、実際には、サブルーチンを作成するのと同じ方法でPerl6で新しい演算子を作成することがいかに簡単であるかを示す一般的な例です。この機能はPerl6に深く組み込まれているため、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 ステートメントではなく && path1 || path2 をテスト) を使用して条件付きロジックを式として表現できるため、C よりも機能的です。スキーム内のすべてが式であるため、スキームはPythonよりも機能的です。
手続き型パラダイムを奨励する言語で関数型スタイルで書くことも、その逆も可能です。言語によって奨励されていないパラダイムで書くことは、より困難であり、かつ/またはより厄介です。
コンピュータサイエンスでは、関数型プログラミングは、計算を数学関数の評価として扱い、状態データや可変データを回避するプログラミングパラダイムです。状態の変化を強調する手続き型プログラミングスタイルとは対照的に、関数の適用を強調します。
手続き型/関数型/目的型プログラミングは、問題へのアプローチ方法に関するものだと思います。
最初のスタイルは、すべてを段階的に計画し、一度に 1 つのステップ (手順) を実装することで問題を解決します。一方、関数型プログラミングは、分割統治アプローチを強調します。このアプローチでは、問題がサブ問題に分割され、各サブ問題が解決され (そのサブ問題を解決する関数が作成されます)、結果が結合されます。問題全体の答えを作成します。最後に、Objective プログラミングは、コンピューター内に多くのオブジェクトを含むミニワールドを作成することで、現実世界を模倣します。各オブジェクトは (やや) ユニークな特性を持ち、他のオブジェクトと相互作用します。それらの相互作用から結果が生まれます。
プログラミングの各スタイルには、独自の長所と短所があります。したがって、「純粋なプログラミング」(つまり、純粋に手続き型 - ところで、これはちょっと奇妙です - または純粋に機能的または純粋に客観的)のようなことを行うことは、不可能ではないにしても非常に困難です。プログラミング スタイルの利点を示すように設計されています (したがって、純粋さが好きな人を「ウィニー」と呼びます:D)。
そして、それらのスタイルから、それぞれのスタイルに最適化するように設計されたプログラミング言語があります。たとえば、Assembly はすべて手続き型です。C、Pascal のような Asm だけでなく (そして Fortran も聞いたことがあります)、ほとんどの初期の言語は手続き型です。それから、有名な Java はすべて目的の学校にあります (実際には、Java と C# も「金銭志向」と呼ばれるクラスに含まれますが、それは別の議論の対象です)。また、対象は Smalltalk です。関数型の学校では、"ほぼ関数型" (不純と見なされるものもある) の Lisp ファミリーと ML ファミリー、および多くの "純粋関数型" の Haskell、Erlang などがあります。ちなみに、Perl、Python などの一般的な言語が多数あります。 、ルビー。
ここであまり強調されていなかった点の 1 つは、Haskell などの最新の関数型言語は、明示的な再帰よりもフロー制御のためのファースト クラス関数に重点を置いているということです。上記で行ったように、Haskell で再帰的に階乗を定義する必要はありません。のようなものだと思います
fac n = foldr (*) 1 [1..n]
は完全に慣用的な構造であり、精神的には明示的な再帰を使用するよりもループを使用することにはるかに近いものです。
Konrad のコメントを拡張するには:
結果として、純粋に関数型のプログラムは入力に対して常に同じ値を生成し、評価の順序は明確に定義されていません。
このため、関数コードは一般に並列化が容易です。関数には (一般に) 副作用がなく、(一般に) 引数に基づいて動作するだけなので、多くの並行性の問題が解消されます。
関数型プログラミングは、コードが正しいことを証明できる必要がある場合にも使用されます。これは、手続き型プログラミングでははるかに困難です (関数型プログラミングでは簡単ではありませんが、それでも簡単です)。
免責事項: 私は関数型プログラミングを何年も使用していませんでしたが、最近また見始めたばかりなので、ここで完全に正しくない可能性があります。:)
手続き型言語は (変数を使用して) 状態を追跡し、一連のステップとして実行する傾向があります。純粋関数型言語は、状態を追跡せず、不変の値を使用し、一連の依存関係として実行される傾向があります。多くの場合、コール スタックのステータスは、手続き型コードの状態変数に格納される情報と同等の情報を保持します。
再帰は、関数型プログラミングの典型的な例です。
コンラッドは次のように述べています。
結果として、純粋に関数型のプログラムは入力に対して常に同じ値を生成し、評価の順序は明確に定義されていません。これは、ユーザー入力やランダム値などの不確実な値を、純粋に関数型言語でモデル化するのが難しいことを意味します。
純粋に機能的なプログラムでの評価の順序は、(特に怠惰な場合) 推論するのが難しいか、重要ではないかもしれませんが、それが明確に定義されていないと言うと、プログラムが進んでいるかどうかわからないように聞こえると思いますまったく働く!
おそらく、関数型プログラムの制御フローは、関数の引数の値がいつ必要になるかに基づいているという説明の方が適切でしょう。これについての良いことは、適切に作成されたプログラムでは、状態が明示的になるということです。各関数は、グローバル状態を恣意的に変更するのではなく、入力をパラメーターとしてリストします。したがって、あるレベルでは、一度に 1 つの関数に関して評価の順序を推論する方が簡単です。各機能は、ユニバースの残りの部分を無視して、実行する必要があることに集中できます。組み合わせた場合、関数は単独の場合と同じように動作することが保証されます[1]。
... ユーザー入力やランダム値などの不確実な値は、純粋に関数型言語でモデル化するのが困難です。
純粋に関数型のプログラムにおける入力の問題の解決策は、十分に強力な抽象化を使用して命令型言語をDSLとして埋め込むことです。命令型 (または非純粋な関数型) 言語では、これは必要ありません。これは、「ごまかして」状態を暗黙的に渡すことができ、評価の順序が明示的であるためです (好むと好まざるとにかかわらず)。この「不正行為」とすべての関数のすべてのパラメーターの強制評価により、命令型言語では、1) 独自の制御フロー メカニズムを (マクロなしで) 作成できなくなります。2) コードは本質的にスレッド セーフではなく、並列化も可能ではありません。デフォルトで、3) そして、元に戻す (タイムトラベル) のようなものを実装するには注意深い作業が必要です (命令型プログラマーは、古い値を戻すためのレシピを保存する必要があります!) 一方で、純粋な関数型プログラミングは、これらすべてのものを購入します。忘れていた—「無料で」。
これが熱狂のように聞こえないことを願っています。命令型プログラミング、特に C# 3.0 のような強力な言語での混合パラダイム プログラミングは、物事を成し遂げるための完全に効果的な方法であり、特効薬はありません。
[1] ... 場合によってはメモリ使用量を除きます (Haskell の foldl と foldl' を参照)。
Konrad のコメントを拡張するには:
評価の順序が明確に定義されていない
一部の関数型言語には、遅延評価と呼ばれるものがあります。つまり、値が必要になるまで関数は実行されません。それまでは、関数自体が渡されます。
手続き型言語は、ステップ 1、ステップ 2、ステップ 3 です... ステップ 2 で 2 + 2 を足すと言うと、その時点でそれが実行されます。遅延評価では 2 + 2 を加算しますが、結果が使用されない場合、加算は行われません。
機会があれば、Lisp/Scheme のコピーを入手して、その中でいくつかのプロジェクトを行うことをお勧めします。最近時流になったアイデアのほとんどは、数十年前に Lisp で表現されたものです: 関数型プログラミング、継続 (クロージャーとして)、ガベージ コレクション、さらには XML です。
したがって、これは、これらの現在のすべてのアイデアに有利なスタートを切るための良い方法であり、記号計算などのいくつかのアイデアもあります。
関数型プログラミングが何に向いていて、何に向いていないかを知っておく必要があります。全てにおいて良いわけではありません。一部の問題は、同じ質問でもいつ質問されるかによって異なる回答が得られるという副作用の観点から表現するのが最適です。
@クレイトン:
Haskellにはproductと呼ばれるライブラリ関数があります:
prouduct list = foldr 1 (*) list
または単に:
product = foldr 1 (*)
したがって、「慣用的な」階乗
fac n = foldr 1 (*) [1..n]
単に
fac n = product [1..n]
手続き型プログラミングは、ステートメントと条件付き構成のシーケンスを、(非機能)値である引数に対してパラメーター化されたプロシージャと呼ばれる個別のブロックに分割します。
関数型プログラミングは、関数がファーストクラスの値であることを除いて同じであるため、他の関数に引数として渡し、関数呼び出しの結果として返すことができます。
関数型プログラミングは、この解釈における手続き型プログラミングの一般化であることに注意してください。ただし、少数派は「関数型プログラミング」を副作用のないことを意味すると解釈します。これはまったく異なりますが、Haskellを除くすべての主要な関数型言語には関係ありません。