手続き型プログラミングと関数型プログラミングの両方についてウィキペディアの記事を読みましたが、まだ少し混乱しています。誰かが核心までそれを煮詰めることができますか?
手続き型プログラミングと関数型プログラミングの両方についてウィキペディアの記事を読みましたが、まだ少し混乱しています。誰かが核心までそれを煮詰めることができますか?
回答:
(理想的には)関数型言語を使用すると、数学関数、つまりn個の引数を取り、値を返す関数を作成できます。プログラムが実行されると、この関数は必要に応じて論理的に評価されます。1
一方、手続き型言語は、一連の一連のステップを実行します。(シーケンシャルロジックを、継続渡しスタイルと呼ばれる機能ロジックに変換する方法があります。)
結果として、純粋に機能的なプログラムは常に入力に対して同じ値を生成し、評価の順序は明確に定義されていません。つまり、ユーザー入力やランダム値などの不確実な値は、純粋に関数型の言語ではモデル化することが困難です。
1この回答の他のすべてと同様に、これは一般化です。このプロパティは、呼び出された場所で順次ではなく結果が必要なときに計算を評価するものであり、「怠惰」と呼ばれます。すべての関数型言語が実際に普遍的に遅延しているわけではなく、遅延が関数型プログラミングに限定されているわけでもありません。むしろ、ここでの説明は、明確で反対のカテゴリではなく流動的なアイデアであるさまざまなプログラミングスタイルについて考えるための「メンタルフレームワーク」を提供します。
基本的に2つのスタイルは、陰と陽のようなものです。1つは整理され、もう1つは無秩序です。関数型プログラミングが明白な選択である状況と、手続き型プログラミングがより良い選択である状況があります。これが、最近2つの言語が開発され、両方のプログラミングスタイルを採用している新しいバージョンが最近登場した理由です。(Perl 6およびD 2)
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;
}
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)
または一行で:
fac n = if n > 0 then n * fac (n-1) else 1
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 }
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
できます)
sub postfix:<!> ($n) { [*] 1..$n }
No operation can have side effects
詳しく説明していただけますか?
sub foo( $a, $b ){ ($a,$b).pick }
←は常に同じ入力に対して同じ出力を返すわけではありませんが、以下はそうですsub foo( $a, $b ){ $a + $b }
他の場所でこの定義が見られたことはありませんが、これはここで与えられた違いをかなりうまくまとめていると思います:
関数型プログラミングは式に焦点を当てています
手続き型プログラミングはステートメントに焦点を当てています
式には値があります。関数型プログラムとは、コンピュータが実行する一連の命令である値を表す式です。
ステートメントには値がなく、代わりにいくつかの概念的なマシンの状態を変更します。
純粋に関数型の言語では、状態を操作する方法がないという意味でステートメントはありません(「ステートメント」という構文構文がまだある可能性がありますが、状態を操作しない限り、この意味でステートメントとは呼びません。 )。純粋に手続き型の言語では、表現はなく、すべてがマシンの状態を操作する命令になります。
状態を操作する方法がないため、Haskellは純粋に関数型の言語の例です。プログラム内のすべてが、マシンのレジスターとメモリーの状態を操作するステートメントであるため、マシンコードは純粋に手続き型言語の例です。
混乱する部分は、プログラミング言語の大部分が式とステートメントの両方を含むため、パラダイムを混在させることができることです。言語は、ステートメントと式の使用をどれだけ促進するかに基づいて、より機能的またはより手続き的なものとして分類できます。
たとえば、Cは関数呼び出しが式であるため、COBOLよりも機能的ですが、COBOLでのサブプログラムの呼び出しはステートメントです(共有変数の状態を操作し、値を返しません)。Pythonは、回路評価を使用して条件付きロジックを式として表現できるため、Cよりも機能的です(ifステートメントではなく&& path1 || path2をテストします)。Scheme内のすべてが式であるため、SchemeはPythonよりも機能的です。
手続き型パラダイムを奨励する言語で関数型スタイルで書くことも、その逆も可能です。言語で推奨されていないパラダイムで書くのは、より難しく、そして/またはより厄介です。
コンピュータサイエンスでは、関数型プログラミングは、計算を数学関数の評価として扱い、状態や可変データを回避するプログラミングパラダイムです。状態の変化を強調する手続き型プログラミングスタイルとは対照的に、関数の適用を強調します。
GetUserContext()
関数を決して入れず、ユーザーコンテキストが渡されます。これは関数型プログラミングですか?前もって感謝します。
手続き型/関数型/客観的プログラミングは、問題への取り組み方に関するものだと思います。
最初のスタイルは、すべてのステップを計画し、一度に1つのステップ(手順)を実装することで問題を解決します。一方、関数型プログラミングでは、問題をサブ問題に分割し、各サブ問題を解決し(そのサブ問題を解決する関数を作成)、結果を結合して、分割統治アプローチを強調します。問題全体の答えを作成します。最後に、客観的プログラミングは、コンピュータ内に多数のオブジェクトを含むミニワールドを作成することで現実の世界を模倣し、各オブジェクトは(やや)ユニークな特性を持ち、他のオブジェクトと相互作用します。それらの相互作用から結果が出てきます。
プログラミングの各スタイルには、独自の長所と短所があります。したがって、「純粋なプログラミング」(つまり、純粋に手続き型-誰かが奇妙な方法でこれを行うことはありません-または純粋に機能的または純粋に客観的)などのことは、いくつかの基本的な問題を除いて、不可能ではないにしても非常に困難ですプログラミングスタイルの利点を実証するために設計されています(したがって、純粋さを好む人を「ウィニー」と呼びます:D)。
次に、それらのスタイルから、いくつかのスタイルごとに最適化されるように設計されたプログラミング言語があります。たとえば、アセンブリはすべて手続き型です。さて、ほとんどの初期の言語は手続き型であり、C、PascalなどのAsmだけではありません(そしてFortranも聞いたようです)。それから、私たちは目的の学校に有名なJavaをすべて持っています(実際、JavaとC#も「お金指向」と呼ばれるクラスにありますが、それは別の議論の対象です)。Smalltalkも目的です。機能的な学校では、「ほぼ機能的」(それらは不純であると見なされているもの)のLispファミリとMLファミリ、および多くの「完全に機能的」なHaskell、Erlangなどがあります。ところで、Perl、Pythonなどの多くの一般的な言語があります。 、ルビー。
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が得られます。
Konradのコメントを拡張するには:
結果として、純粋に機能的なプログラムは常に入力に対して同じ値を生成し、評価の順序は明確に定義されていません。
このため、関数コードは一般に並列化が簡単です。(一般に)関数の副作用はなく、それらは(一般に)引数に作用するだけなので、同時実行性の問題の多くはなくなります。
関数型プログラミングは、コードが正しいことを証明する必要がある場合にも使用されます。これは、手続き型プログラミングでははるかに困難です(関数型プログラミングでは簡単ではありませんが、それでも簡単です)。
免責事項:私は何年も関数型プログラミングを使用していませんが、最近それを再検討し始めたばかりなので、ここでは完全に正しくない場合があります。:)
手続き型言語は、(変数を使用して)状態を追跡し、一連のステップとして実行する傾向があります。純粋に関数型の言語は状態を追跡せず、不変の値を使用し、一連の依存関係として実行される傾向があります。多くの場合、コールスタックのステータスは、手続き型コードの状態変数に格納される情報と同等の情報を保持します。
再帰は関数型プログラミングの古典的な例です。
コンラッドは言った:
結果として、純粋に機能的なプログラムは常に入力に対して同じ値を生成し、評価の順序は明確に定義されていません。つまり、ユーザー入力やランダムな値などの不確実な値は、純粋に関数型の言語ではモデル化することが困難です。
純粋に関数型のプログラムでの評価の順序は、(特に怠惰で)推論するのが難しい場合や、重要でない場合さえありますが、明確に定義されていないと言うと、プログラムがうまくいくかどうかを判断できないように思えます全然働けない!
おそらく、より良い説明は、関数型プログラムの制御フローは、関数の引数の値が必要な場合に基づいているということでしょう。これについての良い点は、適切に記述されたプログラムでは、状態が明示的になることです。各関数は、グローバルな状態を任意に変更するのではなく、入力をパラメーターとしてリストします。したがって、あるレベルでは、一度に1つの関数に関する評価の順序について推論する方が簡単です。各関数は、残りの宇宙を無視して、何をする必要があるかに焦点を合わせることができます。組み合わせると、関数は単独で動作するのと同じように動作することが保証されます[1]。
...ユーザー入力やランダム値などの不確実な値は、純粋に関数型の言語ではモデル化することが困難です。
純粋に機能的なプログラムの入力問題の解決策は、十分に強力な抽象化を使用して、命令型言語をDSLとして埋め込むことです。命令型(または純粋ではない関数型)言語では、状態を「チート」して暗黙的に渡すことができ、評価の順序が(好きかどうかにかかわらず)明示的であるため、これは必要ありません。命令型言語では、すべての関数のすべてのパラメーターが「不正」で強制的に評価されるため、1)独自の制御フローメカニズム(マクロなし)を作成できなくなります。2)コードは本質的にスレッドセーフではなく、並列化できません。デフォルトでは、3)元に戻す(タイムトラベル)などの実装には注意が必要です(命令型プログラマは古い値を取り戻すためのレシピを保存する必要があります!)一方で、純粋な関数型プログラミングはこれらすべてのものを購入します。忘れてしまった—「無料で」。
私はこれが熱狂的なように聞こえないことを望みます、私はいくつかの視点を追加したかっただけです。命令型プログラミング、特にC#3.0のような強力な言語での混合パラダイムプログラミングは、まだ完全に効果的な方法であり、特効薬はありません。
[1] ...おそらくメモリ使用量を除いて(Haskellのfoldlおよびfoldl 'を参照)。
機会があれば、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を除くすべての主要な関数型言語には無関係です。
違いを理解するには、手続き型プログラミングと関数型プログラミングの両方の「ゴッドファーザー」パラダイムが命令型プログラミングであることを理解する必要があります。
基本的に手続き型プログラミングは、抽象化の主要な方法が「手続き」である命令型プログラムを構造化する方法にすぎません。(または一部のプログラミング言語では「関数」)。オブジェクト指向プログラミングも命令プログラムを構造化するもう1つの方法であり、状態はオブジェクトにカプセル化され、「現在の状態」を持つオブジェクトになります。さらに、このオブジェクトには、一連の関数、メソッド、およびその他の機能があり、プログラマーが状態を操作または更新します。
ここで、関数型プログラミングに関して、そのアプローチの要点は、取る値とこれらの値を転送する方法を識別することです。(したがって、状態も変更可能なデータもありません。ファーストクラスの値として関数を受け取り、それらをパラメーターとして他の関数に渡します)。
PS:すべてのプログラミングパラダイムが使用されていることを理解すると、それらすべての違いが明確になります。
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
純粋な関数型言語は一般に研究用言語ですが(現実の世界では無料の副作用が好きです)、現実の手続き型言語では、必要に応じてより単純な関数構文を使用します。
これは通常、Lodashのような外部ライブラリ、またはRustのような新しい言語の組み込みライブラリで実装されます。関数型プログラミングの力仕事は、同様の機能/コンセプトで行われmap
、filter
、reduce
、currying
、partial
、の最後の3つは、あなたはさらに理解するために調べることができます。
実際に使用するには、関数呼び出しのオーバーヘッドが高すぎるため、コンパイラーは通常、関数バージョンを手続きバージョンに内部的に変換する方法を考え出す必要があります。示されている階乗などの再帰的なケースでは、テールコールなどのトリックを使用してO(n)メモリの使用を削除します。副作用がないという事実により、関数型コンパイラは、最後に行われた&& ret
場合でも最適化を実装できます.reduce
。JSでLodashを使用すると、明らかに最適化が許可されないため、パフォーマンスに影響があります(これは通常、Web開発では問題になりません)。Rustのような言語は内部で最適化されます(最適化try_fold
を支援するなどの機能があります&& ret
)。