手続き型プログラミングと関数型プログラミングの違いは何ですか?[閉まっている]


247

手続き型プログラミング関数型プログラミングの両方についてウィキペディアの記事を読みましたが、まだ少し混乱しています。誰かが核心までそれを煮詰めることができますか?


ウィキペディアは、FPが宣言型プログラミングのサブセット(つまり、常に)であることを示唆していますが、これは真実はなく、IPとDPの分類法を膨らませています
シェルビームーアIII

回答:


151

(理想的には)関数型言語を使用すると、数学関数、つまりn個の引数を取り、値を返す関数を作成できます。プログラムが実行されると、この関数は必要に応じて論理的に評価されます。1

一方、手続き型言語は、一連の一連のステップを実行します。(シーケンシャルロジックを、継続渡しスタイルと呼ばれる機能ロジックに変換する方法があります。)

結果として、純粋に機能的なプログラムは常に入力に対して同じ値を生成し評価の順序は明確に定義されていません。つまり、ユーザー入力やランダム値などの不確実な値は、純粋に関数型の言語ではモデル化することが困難です。


1この回答の他のすべてと同様に、これは一般化です。このプロパティは、呼び出された場所で順次ではなく結果が必要なときに計算を評価するものであり、「怠惰」と呼ばれます。すべての関数型言語が実際に普遍的に遅延しているわけではなく、遅延が関数型プログラミングに限定されているわけでもありません。むしろ、ここでの説明は、明確で反対のカテゴリではなく流動的なアイデアであるさまざまなプログラミングスタイルについて考えるための「メンタルフレームワーク」を提供します。


9
ユーザー入力やランダム値などの不確実な値を純粋に関数型言語でモデル化することは困難ですが、これは解決された問題です。モナドを参照してください。
Apocalisp 2008

「関数型プログラムがネストされる一連のステップ」とは、関数の構成を強調することによって関心の分離を提供すること、つまり決定論的計算のサブ計算間の依存関係を分離することを意味します
シェルビームーアIII

これは間違っているようです-プロシージャもネストでき、プロシージャはパラメータを持つことができます
Hurda

1
@ハルダはい、これをよりよく表現できたでしょう。重要なのは、手続き型プログラミングは所定の順序で段階的に行われるのに対し、関数型プログラムは段階的に実行されないということです。むしろ、値は必要なときに計算されます。ただし、一般に合意されているプログラミング用語の定義がないため、このような一般化はほとんど役に立ちません。その点について、私の回答を修正しました。
Konrad Rudolph

97

基本的に2つのスタイルは、陰と陽のようなものです。1つは整理され、もう1つは無秩序です。関数型プログラミングが明白な選択である状況と、手続き型プログラミングがより良い選択である状況があります。これが、最近2つの言語が開発され、両方のプログラミングスタイルを採用している新しいバージョンが最近登場した理由です。Perl 6およびD 2

手続き:

  • ルーチンの出力は、常に入力と直接的な相関関係があるとは限りません。
  • すべてが特定の順序で行われます。
  • ルーチンの実行には副作用がある場合があります。
  • 線形的な方法でソリューションを実装することを強調する傾向があります。

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;
}

機能的:

  • 多くの場合、再帰的です。
  • 指定された入力に対して常に同じ出力を返します。
  • 通常、評価の順序は定義されていません。
  • ステートレスでなければなりません。つまり、操作によって副作用が生じることはありません。
  • 並列実行に最適
  • 分割統治アプローチを強調する傾向があります。
  • 遅延評価の機能がある場合があります。

ハスケル

ウィキペディアからコピー);

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

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 );
  }
}

サイドノート:

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できます)


こんにちは、Perl 6での階乗の実装の例を考慮して、「手続き型」で言及された次の2つのポイントの例を提供できますか?1)ルーチンの出力は、常に入力と直接相関しているわけではありません。2)ルーチンの実行には副作用がある場合があります。
ナガキラン

sub postfix:<!> ($n) { [*] 1..$n }
ブラッド・ギルバート、

@BradGilbert- No operation can have side effects詳しく説明していただけますか?
kushalvm

2
おそらく私が見つけた最良の答えだと思います。そして、私はそれらの個々のポイントについていくつかの調査を行いました。それは本当に私を助けました!:)
ナバニース2015

1
@AkashBisariya sub foo( $a, $b ){ ($a,$b).pick }←は常に同じ入力に対して同じ出力を返すわけではありませんが、以下はそうですsub foo( $a, $b ){ $a + $b }
Brad Gilbert

70

他の場所でこの定義が見られたことはありませんが、これはここで与えられた違いをかなりうまくまとめていると思います:

関数型プログラミングは式に焦点を当てています

手続き型プログラミングはステートメントに焦点を当てています

式には値があります。関数型プログラムとは、コンピュータが実行する一連の命令である値を表す式です。

ステートメントには値がなく、代わりにいくつかの概念的なマシンの状態を変更します。

純粋に関数型の言語では、状態を操作する方法がないという意味でステートメントはありません(「ステートメント」という構文構文がまだある可能性がありますが、状態を操作しない限り、この意味でステートメントとは呼びません。 )。純粋に手続き型の言語では、表現はなく、すべてがマシンの状態を操作する命令になります。

状態を操作する方法がないため、Haskellは純粋に関数型の言語の例です。プログラム内のすべてが、マシンのレジスターとメモリーの状態を操作するステートメントであるため、マシンコードは純粋に手続き型言語の例です。

混乱する部分は、プログラミング言語の大部分が式とステートメントの両方を含むため、パラダイムを混在させることができることです。言語は、ステートメントと式の使用をどれだけ促進するかに基づいて、より機能的またはより手続き的なものとして分類できます。

たとえば、Cは関数呼び出しが式であるため、COBOLよりも機能的ですが、COBOLでのサブプログラムの呼び出しはステートメントです(共有変数の状態を操作し、値を返しません)。Pythonは、回路評価を使用して条件付きロジックを式として表現できるため、Cよりも機能的です(ifステートメントではなく&& path1 || path2をテストします)。Scheme内のすべてが式であるため、SchemeはPythonよりも機能的です。

手続き型パラダイムを奨励する言語で関数型スタイルで書くことも、その逆も可能です。言語で推奨されていないパラダイムで書くのは、より難しく、そして/またはより厄介です。


2
私がウェブで見た中で最高で最も簡潔な説明、ブラボー!
18

47

コンピュータサイエンスでは、関数型プログラミングは、計算を数学関数の評価として扱い、状態や可変データを回避するプログラミングパラダイムです。状態の変化を強調する手続き型プログラミングスタイルとは対照的に、関数の適用を強調します。


4
これは私を最も助けた説明ですが、関数型プログラミングの概念についてはまだあいまいです。実行するために外部オブジェクトの参照に依存しないプログラミングスタイルを探しています(関数を実行するために必要なものはすべてパラメーターとして渡す必要があります)。たとえば、私はGetUserContext()関数を決して入れず、ユーザーコンテキストが渡されます。これは関数型プログラミングですか?前もって感謝します。
Matt Cashatt 2014年

26

手続き型/関数型/客観的プログラミングは、問題への取り組み方に関するものだと思います。

最初のスタイルは、すべてのステップを計画し、一度に1つのステップ(手順)を実装することで問題を解決します。一方、関数型プログラミングでは、問題をサブ問題に分割し、各サブ問題を解決し(そのサブ問題を解決する関数を作成)、結果を結合して、分割統治アプローチを強調します。問題全体の答えを作成します。最後に、客観的プログラミングは、コンピュータ内に多数のオブジェクトを含むミニワールドを作成することで現実の世界を模倣し、各オブジェクトは(やや)ユニークな特性を持ち、他のオブジェクトと相互作用します。それらの相互作用から結果が出てきます。

プログラミングの各スタイルには、独自の長所と短所があります。したがって、「純粋なプログラミング」(つまり、純粋に手続き型-誰かが奇妙な方法でこれを行うことはありません-または純粋に機能的または純粋に客観的)などのことは、いくつかの基本的な問題を除いて、不可能ではないにしても非常に困難ですプログラミングスタイルの利点を実証するために設計されています(したがって、純粋さを好む人を「ウィニー」と呼びます:D)。

次に、それらのスタイルから、いくつかのスタイルごとに最適化されるように設計されたプログラミング言語があります。たとえば、アセンブリはすべて手続き型です。さて、ほとんどの初期の言語は手続き型であり、C、PascalなどのAsmだけではありません(そしてFortranも聞いたようです)。それから、私たちは目的の学校に有名なJavaをすべて持っています(実際、JavaとC#も「お金指向」と呼ばれるクラスにありますが、それは別の議論の対象です)。Smalltalkも目的です。機能的な学校では、「ほぼ機能的」(それらは不純であると見なされているもの)のLispファミリとMLファミリ、および多くの「完全に機能的」なHaskell、Erlangなどがあります。ところで、Perl、Pythonなどの多くの一般的な言語があります。 、ルビー。


26

関数型プログラミング

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が得られます。


5
この例は、関数型プログラミングの「ステートレス」と「不変データ」の用語を理解するのが非常に簡単です。上記のすべての定義と違いを読んでも、この答えを読むまでは混乱を解消できませんでした。ありがとうございました!
マクシムス

13

Konradのコメントを拡張するには:

結果として、純粋に機能的なプログラムは常に入力に対して同じ値を生成し、評価の順序は明確に定義されていません。

このため、関数コードは一般に並列化が簡単です。(一般に)関数の副作用はなく、それらは(一般に)引数に作用するだけなので、同時実行性の問題の多くはなくなります。

関数型プログラミングは、コードが正しいことを証明する必要がある場合にも使用されます。これは、手続き型プログラミングでははるかに困難です(関数型プログラミングでは簡単ではありませんが、それでも簡単です)。

免責事項:私は何年も関数型プログラミングを使用していませんが、最近それを再検討し始めたばかりなので、ここでは完全に正しくない場合があります。:)


12

ここで強調しなかった点の1つは、Haskellなどの最新の関数型言語は、明示的な再帰よりもフロー制御のファーストクラス関数を重視していることです。上記のように、Haskellで階乗を再帰的に定義する必要はありません。のようなものだと思います

fac n = foldr (*) 1 [1..n]

完全に慣用的な構造であり、明示的な再帰を使用するよりも、ループを使用することに精神がはるかに近い。


10

関数型プログラミングは、グローバル変数が使用されない手続き型プログラミングと同じです。


7

手続き型言語は、(変数を使用して)状態を追跡し、一連のステップとして実行する傾向があります。純粋に関数型の言語は状態を追跡せず、不変の値を使用し、一連の依存関係として実行される傾向があります。多くの場合、コールスタックのステータスは、手続き型コードの状態変数に格納される情報と同等の情報を保持します。

再帰は関数型プログラミングの古典的な例です。


1
このページを読んだ後、私は同じことを考えていた->「再帰は関数型プログラミングの古典的な例である」とあなたはそれをクリアしました。
Mudassir Hussain 2014

6

コンラッドは言った:

結果として、純粋に機能的なプログラムは常に入力に対して同じ値を生成し、評価の順序は明確に定義されていません。つまり、ユーザー入力やランダムな値などの不確実な値は、純粋に関数型の言語ではモデル化することが困難です。

純粋に関数型のプログラムでの評価の順序は、(特に怠惰で)推論するのが難しい場合や、重要でない場合さえありますが、明確に定義されていないと言うと、プログラムがうまくいくかどうかを判断できないように思えます全然働けない!

おそらく、より良い説明は、関数型プログラムの制御フローは、関数の引数の値が必要な場合に基づいているということでしょう。これについての良い点は、適切に記述されたプログラムでは、状態が明示的になることです。各関数は、グローバルな状態を任意に変更するのではなく、入力をパラメーターとしてリストします。したがって、あるレベルでは、一度に1つの関数に関する評価の順序について推論する方が簡単です。各関数は、残りの宇宙を無視して、何をする必要があるかに焦点を合わせることができます。組み合わせると、関数は単独で動作するのと同じように動作することが保証されます[1]。

...ユーザー入力やランダム値などの不確実な値は、純粋に関数型の言語ではモデル化することが困難です。

純粋に機能的なプログラムの入力問題の解決策は、十分に強力な抽象化を使用し、命令型言語をDSLとして埋め込むことです。命令型(または純粋ではない関数型)言語では、状態を「チート」して暗黙的に渡すことができ、評価の順序が(好きかどうかにかかわらず)明示的であるため、これは必要ありません。命令型言語では、すべての関数のすべてのパラメーターが「不正」で強制的に評価されるため、1)独自の制御フローメカニズム(マクロなし)を作成できなくなります。2)コードは本質的にスレッドセーフではなく、並列化できません。デフォルトでは、3)元に戻す(タイムトラベル)などの実装には注意が必要です(命令型プログラマは古い値を取り戻すためのレシピを保存する必要があります!)一方で、純粋な関数型プログラミングはこれらすべてのものを購入します。忘れてしまった—「無料で」。

私はこれが熱狂的なように聞こえないことを望みます、私はいくつかの視点を追加したかっただけです。命令型プログラミング、特にC#3.0のような強力な言語での混合パラダイムプログラミングは、まだ完全に効果的な方法であり、特効薬はありません

[1] ...おそらくメモリ使用量を除いて(Haskellのfoldlおよびfoldl 'を参照)。


5

Konradのコメントを拡張するには:

評価の順序は明確に定義されていません

一部の関数型言語には、遅延評価と呼ばれるものがあります。つまり、値が必要になるまで関数は実行されません。それまでは、関数自体が渡されます。

手続き型言語はステップ1、ステップ2、ステップ3です。ステップ2で2 + 2を追加すると言った場合、それはその時点で行われます。遅延評価では、加算2 + 2と言いますが、結果が使用されない場合、加算は行われません。


4

機会があれば、Lisp / Schemeのコピーを入手し、その中でいくつかのプロジェクトを行うことをお勧めします。最近バンドワゴンになったアイデアのほとんどは、数十年前のLispで表現されました:関数型プログラミング、継続(クロージャーとして)、ガベージコレクション、さらにはXMLです。

ですから、これらは現在のこれらすべてのアイデア、そしてシンボリック計算のような他のいくつかのアイデアに有利なスタートを切る良い方法です。

関数型プログラミングが何に適しているのか、何が良くないのかを知っておく必要があります。それはすべてに適しているわけではありません。いくつかの問題は、副作用の観点から最もよく表されます。同じ質問は、質問されたときに応じて異なる答えを与えます。


3

@クレイトン:

Haskellにはproductというライブラリ関数があります

prouduct list = foldr 1 (*) list

または単に:

product = foldr 1 (*)

したがって、「慣用」階乗

fac n = foldr 1 (*)  [1..n]

単になります

fac n = product [1..n]

これは質問に対する答えを提供しません。批評したり、著者に説明を求めるには、投稿の下にコメントを残してください。
Nick Kitto

私はあなたがそれを信じることができれば、これは、コメントシステムが追加される前に、何年も前に投稿されたと信じて:stackoverflow.com/help/badges/30/beta?userid=2543
ジャレドアップダイク

2

手続き型プログラミングは、一連のステートメントと条件付き構成体を、(機能しない)値である引数に対してパラメーター化されたプロシージャと呼ばれる個別のブロックに分割します。

関数型プログラミングは同じですが、関数がファーストクラスの値であるため、他の関数に引数として渡し、関数呼び出しの結果として返すことができます。

関数型プログラミングは、この解釈における手続き型プログラミングの一般化であることに注意してください。ただし、少数派は「関数型プログラミング」を副作用のないものとして解釈しています。これはまったく異なりますが、Haskellを除くすべての主要な関数型言語には無関係です。


1

違いを理解するには、手続き型プログラミングと関数型プログラミングの両方の「ゴッドファーザー」パラダイムが命令型プログラミングであることを理解する必要があります

基本的に手続き型プログラミングは、抽象化の主要な方法が「手続き」である命令型プログラムを構造化する方法にすぎません。(または一部のプログラミング言語では「関数」)。オブジェクト指向プログラミングも命令プログラムを構造化するもう1つの方法であり、状態はオブジェクトにカプセル化され、「現在の状態」を持つオブジェクトになります。さらに、このオブジェクトには、一連の関数、メソッド、およびその他の機能があり、プログラマーが状態を操作または更新します。

ここで、関数型プログラミングに関して、そのアプローチの要点は、取る値とこれらの値を転送する方法を識別することです。(したがって、状態も変更可能なデータもありません。ファーストクラスの値として関数を受け取り、それらをパラメーターとして他の関数に渡します)。

PS:すべてのプログラミングパラダイムが使用されていることを理解すると、それらすべての違いが明確になります。

PSS:結局のところ、プログラミングパラダイムは問題を解決するための単なる異なるアプローチです。

PSS:この quoraの回答は素晴らしい説明があります。


0

ここでの答えはどれも、慣用的な関数型プログラミングを示していません。再帰的な階乗の答えは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のような新しい言語の組み込みライブラリで実装されます。関数型プログラミングの力仕事は、同様の機能/コンセプトで行われmapfilterreducecurryingpartial、の最後の3つは、あなたはさらに理解するために調べることができます。

補遺

実際に使用するには、関数呼び出しのオーバーヘッドが高すぎるため、コンパイラーは通常、関数バージョンを手続きバージョンに内部的に変換する方法を考え出す必要があります。示されている階乗などの再帰的なケースでは、テールコールなどのトリックを使用してO(n)メモリの使用を削除します。副作用がないという事実により、関数型コンパイラは、最後に行われた&& ret場合でも最適化を実装できます.reduce。JSでLodashを使用すると、明らかに最適化が許可されないため、パフォーマンスに影響があります(これは通常、Web開発では問題になりません)。Rustのような言語は内部で最適化されます(最適化try_foldを支援するなどの機能があります&& ret)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.