関数型プログラミングの利点を説明する多くの記事で、Haskell、ML、ScalaまたはClojureなどの関数型プログラミング言語、 "宣言型言語"と呼ばれる "宣言型言語"など)を見てきました。 C/C++/C#/ Java。私の質問は、関数型プログラミング言語を命令型ではなく宣言型にする理由です。
宣言型プログラミングと命令型プログラミングの違いを説明するためによく遭遇する説明は、命令型プログラミングでは、宣言型言語の「何をすべきか」ではなく「何かを行う方法」をコンピュータに伝えることです。この説明で私が抱えている問題は、あなたがすべてのプログラミング言語で常に両方を行っているということです。最下位レベルのアセンブリに進んでも、コンピュータに "何をすべきか"と伝え、CPUに2つの数値を追加するように指示します。追加の実行方法については指示しません。スペクトルのもう一方の端、Haskellのような高レベルの純粋な関数型言語に行くと、実際には、特定のタスクを達成する方法をコンピューターに指示しています。つまり、プログラムは、特定のタスクを達成するための一連の命令です。コンピュータは一人で達成する方法を知りません。 Haskell、Clojureなどの言語はC/C++/C#/ Javaよりも明らかにレベルが高く、遅延評価、不変データ構造、匿名関数、カリー化、永続データ構造などの機能を提供することを理解しています関数型プログラミングは可能で効率的ですが、それらを宣言型言語として分類することはしません。
私にとって純粋な宣言型言語は完全に宣言のみで構成される言語です。そのような言語の例はCSSです(はい、CSSは技術的にはプログラミング言語ではないことを知っています)。 CSSには、ページのHTMLおよびJavaScriptで使用されるスタイル宣言が含まれているだけです。 CSSは宣言を行う以外は何もできません。クラス関数を作成できません。つまり、いくつかのパラメーターに基づいて表示するスタイルを決定する関数、CSSスクリプトを実行できませんなど。 プログラミング言語)。
更新:
私は最近Prologをいじってみましたが、Prologは完全に宣言型のプログラミング言語だけではないとしても、(少なくとも私の意見では)完全に宣言型の言語に最も近いプログラミング言語です。 Prologでプログラミングを詳しく説明するには、ファクト(特定の入力に対してtrueを返す述語関数)またはルール(入力に基づいて特定の条件/パターンに対してtrueを返す述語関数)、ルールのいずれかを宣言するパターンマッチング技術を使用して定義されます。プロローグで何かを行うには、述語の1つ以上の入力を変数で置き換えることによって知識ベースを照会し、プロローグは、述語が成功する変数の値を見つけようとします。
私の要点はプロローグにあり、必須の指示はありません。基本的に、コンピューターが知っていることをコンピューターに伝え(宣言)、知識について質問(クエリ)します。関数型プログラミング言語では、メモリの場所を直接操作していない場合や計算を段階的に記述していない場合でも、値を取得したり、関数Xを呼び出して1を追加したりするなど、引き続き命令を出します。 Haskell、ML、ScalaまたはClojureでのプログラミングは、私が間違っているかもしれませんが、この意味で宣言的であるとは言いません。適切で、真の、純粋な関数型プログラミングは、上述。
あなたは物事を宣言することと機械に指示することの間に線を引いているようです。そのような厳格な分離はありません。命令型プログラミングで指示されているマシンは物理的なハードウェアである必要はないので、解釈の自由は非常にあります。ほとんどすべてが、適切な抽象マシン用の明示的なプログラムと見なすことができます。たとえば、CSSは、主にセレクターを解決し、このように選択されたDOMオブジェクトの属性を設定するマシンをプログラミングするためのかなり高級な言語と見なすことができます。
問題は、そのような見方が理にかなっているかどうか、そして逆に、命令のシーケンスが計算されている結果のdeclarationにどの程度似ているかです。 CSSの場合、宣言的パースペクティブの方が明らかに便利です。 Cの場合、命令型の視点が明らかに優先されます。 Haskellのような言語については…
言語は、具体的なセマンティクスを指定しています。つまり、もちろんプログラムを一連の操作として解釈することができます。プリミティブ操作を選択して商品ハードウェアに適切にマッピングするために、それほどの労力を費やすことすらありません(これは、STGマシンと他のモデルが行うことです)。
ただし、Haskellプログラムの作成方法は、計算される結果の説明として頻繁にcanと読みます。たとえば、最初のN個の階乗の合計を計算するプログラムを考えてみます。
sum_of_fac n = sum [product [1..i] | i <- ..n]
これをデ糖化して、STG操作のシーケンスとして読み取ることができますが、結果の説明として読み取るのがはるかに自然です(これは、宣言型プログラミングのより有用な定義です) 「何を計算するか」):結果は、すべてのi
= 0、…、nに対する[1..i]
の積の合計です。そして、ほとんどすべてのCプログラムまたは関数よりもはるかに宣言的です。
命令型プログラムの基本単位はstatementです。ステートメントはその副作用のために実行されます。受け取った状態を変更します。一連のステートメントは一連のコマンドであり、これを実行することを意味します。プログラマは、計算を実行する正確な順序を指定します。これは、人々にコンピュータに伝えることによって何を意味するかhowそれを行うために。
宣言型プログラムの基本単位は式です。式には副作用がありません。入力状態を変更するのではなく、入力と出力の関係を指定し、入力から新しい別個の出力を作成します。式のシーケンスは、それらの間の関係を指定する式が含まれていないと意味がありません。プログラマーはデータ間の関係を指定し、プログラムはそれらの関係から計算を実行する順序を推測します。これは、人々にコンピュータにwhatを指示することによって意味されます。
命令型言語には式がありますが、物事を成し遂げるための主な手段はステートメントです。同様に、宣言型言語には、Haskellのdo
表記内のモナドシーケンスのような、ステートメントに似たセマンティクスを持ついくつかの式がありますが、それらのコアでは、それらは1つの大きな式です。これにより、初心者は非常に命令的に見えるコードを書くことができますが、そのパラダイムを脱出するときに言語の真の力が生まれます。
宣言型プログラミングと命令型プログラミングを区別する実際の定義特性は、シーケンス型の命令ではない宣言型スタイルです。下位レベルでは、CPUはこのように動作しますが、コンパイラーの懸念事項です。
CSSは「宣言型言語」であるとおっしゃっていますが、私はそれを言語とは呼びません。これは、JSON、XML、CSV、INIなどのデータ構造形式であり、ある種のインタープリターに知られているデータを定義するための形式にすぎません。
言語から代入演算子を削除すると、いくつかの興味深い副作用が発生します。これが、宣言型言語でstep1-step2-step3命令命令をすべて失う本当の原因です。
関数の代入演算子で何をしますか?中間ステップを作成します。それが肝心な点です。代入演算子は、データを段階的に変更するために使用されます。データを変更できなくなるとすぐに、これらの手順がすべて失われ、次のようになります。
すべての関数にはステートメントが1つだけあり、このステートメントは単一の宣言です
単一のステートメントを多くのステートメントのように見せるための方法はたくさんありますが、それは単なるトリックです。たとえば、1 + 3 + (2*4) + 8 - (7 / (8*3))
これは明らかに単一のステートメントですが、次のように記述した場合...
_1 +
3 +
(
2*4
) +
8 -
(
7 / (8*3)
)
_
それは、作者が意図していた望ましい分解を脳が認識しやすくする一連の操作の外観をとることができます。私はよくC#でこれを次のようなコードで行います。
_people
.Where(person => person.MiddleName != null)
.Select(person => person.MiddleName)
.Distinct();
_
これは単一のステートメントですが、多くに分解されているように見えます。割り当てがないことに注意してください。
また、上記のどちらの方法でも、コードが実際に実行される方法は、すぐに読み取る順序ではありません。このコードは何をすべきかを示していますが、howを指示するものではないためです。上記の単純な計算は明らかに左から右に書かれた順序で実行されないことに注意してください。上記のC#の例では、これらの3つのメソッドは実際にはすべて再入可能であり、順序どおりに実行されず、コンパイラは実際にコードを生成しますそれはそのステートメントwantsを実行しますが、必ずしもhowとは限りません。
私は、宣言型コーディングのこの中間ステップのないアプローチに慣れることが、その中で最も難しい部分だと信じています。そして、Haskellのように本当にそれを許可しない言語がほとんどないため、Haskellがそれほどトリッキーである理由。合計などの中間変数を使用して通常実行することを実行するには、興味深い体操を開始する必要があります。
宣言型言語では、中間変数で何かをしたい場合-それは、そのことを行う関数にパラメーターとして渡すことを意味します。これが、再帰が非常に重要になる理由です。
sum xs = (head xs) + sum (tail xs)
resultSum
変数を作成してxs
をループするときに変数に追加することはできません。最初の値を取得し、それ以外のすべての合計に追加する必要があります。それ以外のすべてにアクセスするには、xs
を関数tail
に渡す必要があります。これは、xsの変数を作成して頭からポップするだけでは不可能であり、これは必須のアプローチです。 (はい、私はあなたが破壊を使用できることを知っていますが、この例は説明のためのものです)
パーティーに遅れるのはわかっているけど、先日ひらめきがあったのでここに行く….
宣言型プログラミングと命令型プログラミングの違いを説明したり、宣言型プログラミングの意味を説明したりすると、機能的で不変であり、副作用がないというコメントに間違いがあると思います。また、あなたの質問で述べているように、「何をすべきか」と「どのようにそれを行うか」の全体はあいまいすぎて、実際にはどちらも説明しません。
シンプルなコードa = b + c
をベースにして、いくつかの異なる言語でステートメントを表示し、アイデアを得ましょう。
Cのような命令型言語でa = b + c
を書くときは、変数a
にcurrentb + c
の値を代入し、それ以外は何もしません。 a
が何であるかについての根本的な発言はしていません。むしろ、単にプロセス内のステップを実行しているだけです。
Microsoft Excelなどの宣言型言語でa = b + c
を書く場合(そうです、Excel isはプログラミング言語であり、おそらくそれらすべての中で最も宣言型です)関係a
、b
とc
の間。これは、常にa
が他の2つの合計である場合に当てはまります。これはプロセスのステップではなく、不変条件、保証、真実の宣言です。
関数型言語も宣言型ですが、ほとんど偶然です。たとえば、Haskelではa = b + c
も不変の関係を表明しますが、これはb
とc
が不変だからです。
つまり、オブジェクトが不変で、関数に副作用がない場合、コードは宣言型になります(たとえ命令コードと同じに見えても)、それは重要ではありません。また、割り当てを避けていません。宣言型コードのポイントは、関係についての基本的なステートメントを作成することです。
コンピュータにwhatを実行するよう指示することとhowを実行するよう指示することの間に明確な区別がないことは、あなたの言うとおりです。
ただし、スペクトルの片側では、ほとんど独占的にメモリの操作方法について考えます。つまり、問題を解決するには、「このメモリの場所をxに設定してから、そのメモリの場所をyに設定し、メモリの場所zにジャンプする...」のような形式でコンピュータに提示すると、どういうわけか、他のいくつかのメモリ位置に結果。
Java、C#などのマネージ言語では、ハードウェアメモリに直接アクセスすることはできません。命令型プログラマーは、クラスインスタンスの静的変数、参照、またはフィールドに関心を持っています。これらはすべて、メモリ位置のある程度の省略です。
Haskell、OTOHのような言語では、記憶は完全になくなります。それは単にそうではありません
f a b = let y = sum [a..b] in y*y
引数aとbを保持する2つのメモリセルと、中間結果yを保持する別のメモリセルが必要です。確かに、コンパイラのバックエンドはこのように機能する最終的なコードを出力する可能性があります(ある意味で、ターゲットアーキテクチャがNeumannマシンである限り、そうする必要があります)。
しかし、ポイントは、上記の機能を理解するためにv。Neumannアーキテクチャを内部化する必要がないことです。それを実行するのに現代的なコンピューターも必要ありません。たとえば、純粋なFP言語でプログラムをSKI計算のベースで動作する仮想マシンに変換するのは簡単です。次に、Cプログラムで同じことを試してください!
私にとって純粋な宣言型言語は、完全に宣言のみで構成されている言語でしょう
これは十分ではありません、私見。 Cプログラムでさえ、一連の宣言にすぎません。私たちは宣言をさらに修飾する必要があると感じています。たとえば、何かis(宣言的)またはdoes(命令的)を教えてくれますか。