手続き型と機能型の違いを真に理解する


114

手続きプログラミングと関数型プログラミングの違いを理解するのに本当に苦労しています。

次に、関数型プログラミングに関するWikipediaエントリの最初の2つの段落を示します

コンピュータサイエンスでは、関数型プログラミングは、計算を数学関数の評価として扱い、状態や可変データを回避するプログラミングパラダイムです。状態の変化を強調する命令型プログラミングスタイルとは対照的に、関数の適用を強調します。関数型プログラミングは、関数定義、関数適用、および再帰を調査するために1930年代に開発された正式なシステムであるラムダ計算にルーツがあります。多くの関数型プログラミング言語は、ラムダ計算の詳細と見なすことができます。

実際には、数学関数と命令型プログラミングで使用される「関数」の概念の違いは、命令型関数には副作用があり、プログラムの状態の値を変更する可能性があることです。このため、それらには参照透過性がありません。つまり、同じ言語式は、実行中のプログラムの状態に応じて、異なる時点で異なる値になる可能性があります。逆に、関数コードでは、関数の出力値は関数に入力された引数にのみ依存するfため、引数に同じ値を指定して関数を2回呼び出す xと、同じ結果が生成されますf(x)両方の時間。副作用をなくすことで、プログラムの動作の理解と予測がはるかに容易になります。これは、関数型プログラミングを開発するための主要な動機の1つです。

それが言うパラグラフ2で

逆に、関数コードでは、関数の出力値は関数に入力された引数にのみ依存するfため、引数に同じ値を指定して関数を2回呼び出すxと、同じ結果がf(x)両方とも生成されます。

これは、手続き型プログラミングの場合とまったく同じではありませんか?

際立っている手続き型と機能型のどちらを探すべきですか?


1
Abafeiからの「魅力的なPython:Pythonでの関数型プログラミング」リンクが壊れていました。ここでは、リンクの良いセットは次のとおりです。ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
クリスKoknat

これの別の側面は命名です。例えば。JavaScriptおよびCommon Lispでは、副作用が許可されている場合でも関数という用語を使用します。Schemeでは同じことが一貫してプロシージャと呼ばれます。純粋なCL関数は、純粋な関数型スキーマプロシージャとして記述できます。Schemeに関するほとんどすべての本は、標準で使用されているtyermであり、手続き的または機能的であるということとは何の関係もないので、プロシージャという用語を使用しています。
Sylwester、2015

回答:


276

関数型プログラミング

関数型プログラミングとは、関数を値として扱う機能を指します。

「通常の」値との類似を考えてみましょう。2つの整数値を取り、+演算子を使用してそれらを組み合わせて新しい整数を取得できます。または、整数に浮動小数点数を乗算して、浮動小数点数を取得することもできます。

関数型プログラミングでは、composeliftなどの演算子を使用して、2つの関数値を組み合わせて新しい関数値を生成できます。または、関数値とデータ値を組み合わせて、mapfoldなどの演算子を使用して新しいデータ値を生成することもできます。

多くの言語には関数型プログラミング機能があることに注意してください。通常、関数型言語とは考えられていない言語も含まれます。Grandfather FORTRANも関数値をサポートしていましたが、関数結合演算子の機能はあまりありませんでした。言語が「関数型」と呼ばれるためには、関数型プログラミング機能を大きく受け入れる必要があります。

手続き型プログラミング

手続き型プログラミングとは、一般的な一連の命令を1つのプロシージャにカプセル化して、コピーアンドペーストすることなく、多くの場所からそれらの命令を呼び出せるようにする機能を指します。手順はプログラミングのごく初期の開発であったため、この機能はほぼ常に、機械語またはアセンブリ言語のプログラミングで要求されるプログラミングのスタイルと関連付けられています。これは、格納場所の概念と、それらの場所の間でデータを移動する命令を強調するスタイルです。

コントラスト

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

この定義は完全に機能し、次のことを行います。

  1. compose(odd, length)oddlength関数を組み合わせて、文字列の長さが奇数かどうかを決定する新しい関数を生成します。
  2. map(..., words)は、の各要素に対してその新しい関数を呼び出しwords、最終的にブール値の新しいリストを返します。それぞれのリストは、対応する単語に奇数の文字があるかどうかを示します。
  3. apply(and, ...)「and」演算子を結果のリストに適用し、すべてのブール値 -ingして最終結果を生成します。

これらの例から、手続き型プログラミングは変数内の値の移動と、最終結果を生成するために必要な操作の明示的な記述に非常に関係していることがわかります。対照的に、関数型スタイルは、初期入力を最終出力に変換するために必要な関数の組み合わせを強調します。

この例では、手続き型コードと関数型コードの一般的な相対サイズも示しています。さらに、手続き型コードのパフォーマンス特性は、関数型コードのパフォーマンス特性よりも見やすい場合があることを示しています。考慮してください:関数はリスト内のすべての単語の長さを計算しますか、それとも最初の偶数の長さの単語を見つけた直後にそれぞれ停止しますか?一方、関数コードは、明示的なアルゴリズムではなく主に意図を表現するため、高品質の実装でかなり深刻な最適化を実行できます。

参考文献

この質問はたくさん出てきます...たとえば、次を参照してください:

ジョン・バッカスのチューリング賞講演では、関数型プログラミングの動機を詳しく説明しています。

プログラミングはフォンノイマンスタイルから解放されますか?

現在の状況では、この論文については触れないでください。かなり技術的に、かなり早くなります。本当に根本的だと思うので我慢できませんでした。


補遺-2013

コメンテーターは、人気のある現代の言語は、手続き型や関数型に加えて、他のプログラミングスタイルも提供していると指摘しています。このような言語は、多くの場合、次のプログラミングスタイルの1つ以上を提供します。

  • クエリ(例:内包表記、言語統合クエリ)
  • データフロー(例:暗黙の反復、バルク操作)
  • オブジェクト指向(カプセル化されたデータやメソッドなど)
  • 言語指向(アプリケーション固有の構文、マクロなど)

この応答の疑似コードの例が、他のスタイルから利用できるいくつかの機能からどのように利益を得ることができるかの例については、以下のコメントを参照してください。特に、手続き型の例は、事実上すべての高レベルの構造の適用から利益を得ます。

示されている例は、議論中の2つのスタイルの違いを強調するために、これらの他のプログラミングスタイルの意図的な混在を避けています。


1
確かにいい答えですが、コードを少し簡略化できます。たとえば、次のようにします。 "function allOdd(words){foreach(auto word in words){odd(length(word)?return false
:;

Pythonの「関数型スタイル」と比較すると、関数型のスタイルはかなり読みにくいです:def odd_words(words):return [x for x in words if odd(len(x))]
boxed

@boxed:あなたのodd_words(words)定義は答えとは異なる何かをしますallOdd。フィルタリングとマッピングにはリスト内包表記がよく使用されますが、ここでは関数allOddによって単語のリストが単一のブール値に削減されることになっています。
ShinNoNoir

@WReach:次のように機能的な例を記述します。function allOdd(words){return and(odd(length(first(words)))、allOdd(rest(words))); これは例よりもエレガントではありませんが、末尾再帰言語では、命令型スタイルと同じパフォーマンス特性を持っています。
mishoo 2013

@mishoo言語は、仮定が成立するためには、and演算子で末尾再帰的かつ厳密かつ短絡的である必要があると思います。
kqr

46

関数型プログラミングと命令型プログラミングの本当の違いは考え方です-命令型プログラマは変数とメモリのブロックを考えていますが、関数型プログラマは「入力データを出力データに変換するにはどうすればよいですか」と考えています-「プログラム」はパイプラインですデータの変換セットを使用して、入力から出力にデータを取り込みます。これはIMOの興味深い部分であり、「変数を使用してはならない」ビットではありません。

この考え方の結果として、FPプログラムは、一般的に記述するものの代わりに、特定の機構から、どうなるの、それが起こるのだろう-もしので、私たちはできるはっきり状態何「選択」と「」と「集計」と、これは強力ですが、私たちをAsParallel()と同じように、実装を自由に入れ替えることができ、突然、シングルスレッドアプリがnコアにスケールアウトします。


コード例スニペットを使用して2つを対比できる方法はありますか?本当に感謝
Philoxopher

1
@KerxPhilo:これは非常に単純なタスクです(1からnまでの数を追加)。必須:現在の数を変更し、これまでの合計を変更します。コード:int i、sum; 合計= 0; for(i = 1; i <= n; i ++){sum + = i; }。関数型(Haskell):数字の遅延リストを取り、ゼロに追加しながらそれらを一緒に折りたたみます。コード:foldl(+)0 [1..n]。コメントに書式はありません。
Dirkt

答えに+1。言い換えれば、関数型プログラミングとは、可能な場合は常に副作用のない関数を記述することです。つまり、同じパラメーターを指定すると、関数は常に同じものを返します-これが基盤です。そのアプローチを極端に実行すると、副作用(常に必要です)が分離され、残りの関数は入力データを出力データに変換するだけです。
ベルチン2017

12
     Isn't that the same exact case for procedural programming?

いいえ、手続き型コードには副作用がある可能性があるためです。たとえば、呼び出し間の状態を保存できます。

とはいえ、手続き型と見なされる言語でこの制約を満たすコードを作成することは可能です。また、関数型と見なされる一部の言語でこの制約を破るコードを記述することもできます。


1
例と比較を見せてください。できれば本当に感謝しています。
Philoxopher 2011年

8
Cのrand()関数は、呼び出しごとに異なる結果を提供します。呼び出し間の状態を保存します。参照的に透過的ではありません。対照的に、C ++のstd :: max(a、b)は、同じ引数を指定すると常に同じ結果を返し、副作用(私が知っていること)はありません。
アンディトーマス

11

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

最初に注意すべきことは、彼が合同していることです:

  • 機能的
  • 表現指向と
  • イテレーター中心

プログラミング、そして典型的な関数型スタイルよりも明確な制御フローを持つ反復型プログラミングの機能がありません。

これらについて簡単に説明しましょう。

式中心のスタイルとは、物事を可能な限り評価して物事を評価するスタイルです。関数型言語は式を愛することで有名ですが、実際には、合成可能な式なしで関数型言語を使用することは可能です。私は表現がなく、単に声明があるところを作り上げます。

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

これは、関数がステートメントとバインディングのチェーンを介して純粋にチェーン化されていることを除いて、前に示したものとほとんど同じです。

イテレータ中心のプログラミングスタイルは、Pythonで採用されているものかもしれません。純粋に反復的な、イテレータ中心のスタイルを使用してみましょう:

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

だから何差が間にある場合は関数型言語のポイントは:

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)

関数のグローバルな依存関係を明示的に制御していない場合、これは単純ではなく、おそらく不可能です。関数型プログラミングの最大の特徴は、より一般的な抽象を一貫して作成でき、それらを組み合わせてより大きな全体にできることを信頼できることです。


あなたが知っている、私はかなり確信しているapplyと全く同じ操作ではありませんfoldか、reduce私は非常に一般的なアルゴリズムを持っている素敵な能力に同意するものの、。
ベネディクトリー

またはapplyを意味することは聞いたことがありませんが、ブール値を返すには、このコンテキストにある必要があるように見えます。foldreduce
Veedrac、2014

ああ、わかりました、名前がわかりませんでした。片付けていただきありがとうございます。
ベネディクトリー

6

手続き型パラダイム(代わりに「構造化プログラミング」と言いましょうか)では、可変メモリと、それをある順序で(次々に)読み書きする命令を共有しています。

関数型のパラダイムでは、変数と関数があります(数学的な意味では、変数は時間とともに変化せず、関数は入力に基づいてのみ計算できます)。

(これは過度に単純化されています。たとえば、FPLは通常、可変メモリを操作するための機能を備えていますが、手続き型言語は高次のプロシージャをサポートできることが多いため、それほど明確ではありませんが、これはアイデアを与えるはずです)



2

関数型プログラミングでは、シンボル(変数名または関数名)の意味を推論するために、2つのこと、つまり現在のスコープとシンボル名を知るだけで十分です。不変性の純粋な関数型言語がある場合、これらは両方とも「静的」(ひどくオーバーロードされた名前では申し訳ありません)の概念です。つまり、ソースコードを見るだけで、現在のスコープと名前の両方を確認できます。

手続き型プログラミングで、背後にある価値は何かという質問に答えるには、xそこに到達した方法を知る必要もあります。スコープと名前だけでは十分ではありません。この実行パスは「ランタイム」プロパティであり、非常に多くの異なるものに依存する可能性があるため、これは私が最大の課題と見なすものです。ほとんどの人は、デバッグするだけで実行パスを回復しようとしないことを学びます。


1

私は最近、表現の問題という点で違いを考えています。 Phil Wadlerの説明はよく引用されますが、この質問への受け入れられた答えはおそらく理解しやすいでしょう。基本的に、命令型言語は問題に対して1つのアプローチを選択する傾向がある一方、関数型言語は他のアプローチを選択する傾向があるようです。


0

2つのプログラミングパラダイムの1つの明確な違いは、状態です。

関数型プログラミングでは、状態は回避されます。簡単に言えば、値が割り当てられている変数はありません。

例:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

ただし、手続き型プログラミングでは状態を使用します。

例:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.