関数型プログラミング言語が状態を保存できない場合、ユーザーからの入力を読み取るなどの単純なことをどのように実行しますか?入力をどのように「保存」するか(またはそのデータを保存するか)。
たとえば、この単純なCの事柄は、Haskellのような関数型プログラミング言語にどのように変換されますか?
#include<stdio.h>
int main() {
int no;
scanf("%d",&no);
return 0;
}
(私の質問はこの優れた投稿に触発されました: "名詞の王国での実行" これを読んだことで、正確にオブジェクト指向プログラミングは、どのようにJavaが1つの極端な方法でそれを実装するか、および関数型プログラミング言語がどのように対照的であるかです。)
ここにはたくさんの良い答えがありますが、それらは長いです。役立つ短い答えを出そうと思います。
関数型言語は、Cと同じ場所、つまり名前付き変数とヒープに割り当てられたオブジェクトに状態を配置します。違いは次のとおりです。
関数型言語では、「変数」はスコープに入ると(関数呼び出しまたはlet-bindingを介して)初期値を取得し、その値は後で変更されません。同様に、ヒープに割り当てられたオブジェクトは、そのすべてのフィールドの値ですぐに初期化され、その後は変更されません。
「状態の変化」は、既存の変数またはオブジェクトを変更するのではなく、新しい変数をバインドするか、新しいオブジェクトを割り当てることによって処理されました。
IOはトリックで機能します。文字列を生成する副作用の計算は、Worldを引数として取り、文字列と新しいWorldを含むペアを返す関数によって記述されます。世界には、すべてのディスクドライブの内容、これまでに送受信されたすべてのネットワークパケットの履歴、画面上の各ピクセルの色などが含まれます。トリックの鍵は、世界へのアクセスが慎重に制限されているため、
プログラムは世界のコピーを作成できません(どこに配置しますか?)
世界を捨てることができるプログラムはない
このトリックを使用すると、時間の経過とともに状態が変化する1つのユニークな世界が存在できるようになります。関数型言語で記述されたnotである言語ランタイムシステムは、固有のワールドを更新する代わりに更新することにより、副作用のある計算を実装します。新しいもの。
このトリックは、サイモンペイトンジョーンズとフィルワドラーの画期的な論文 "Imperative Function Programming" で美しく説明されています。
より多くのスペースを確保するために、新しい回答に対するコメントの返信を打ち切ります。
私が書いた:
私の知る限り、この
IO
ストーリー(World -> (a,World)
)は、Haskellに適用した場合の神話です。HaskellのIO
タイプは純粋に逐次計算のみを説明しているためです同時実行が含まれます。 「純粋に連続的」とは、命令計算の開始と終了の間で、その計算によるものを除いて、世界(宇宙)でさえ変更が許可されないことを意味します。たとえば、コンピュータが離れている間、脳などは動きません。並行性はWorld -> PowerSet [(a,World)]
のようなもので処理でき、非決定性とインターリーブが可能になります。
ノーマンは書きました:
@Conal:IOストーリーは、非決定性とインターリーブにかなりうまく一般化されていると思います。私が正しく覚えていれば、「厄介な分隊」の論文にかなり良い説明があります。しかし、私は知りません真の並列処理を明確に説明する優れた論文。
@ノーマン:どのような意味で一般化しますか?非決定性と同時実行性を考慮していないため、通常与えられる表記モデル/説明、World -> (a,World)
はHaskell IO
と一致しないことをお勧めします。 World -> PowerSet [(a,World)]
のように適合するより複雑なモデルがあるかもしれませんが、そのようなモデルがうまく機能し、適切で一貫性があることが示されているかどうかはわかりません。 IO
にFFIでインポートされた何千もの命令型API呼び出しが含まれていることを考えると、個人的にはそのような獣が見つかるかもしれません。そのため、IO
はその目的を果たしています。
未解決の問題:
IO
モナドがHaskellの罪箱になりました。 (私たちが何かを理解できないときはいつでも、IOモナドでそれを投げます。)
(Simon PJのPOPLトークよりヘアシャツの着用ヘアシャツの着用:Haskellの回顧展。)
厄介な分隊への取り組みのセクション3.1で、Simonはtype IO a = World -> (a, World)
で機能しないことを指摘しています。 「同時実行性を追加すると、このアプローチはうまくスケーリングしません」を含みます。次に、可能な代替モデルを提案し、次に、説明的な説明の試みを放棄して、
ただし、代わりに、プロセス計算のセマンティクスへの標準的なアプローチに基づいて、運用セマンティクスを採用します。
正確で有用な表記モデルを見つけることができないこの失敗は、Haskell IOが「関数型プログラミング」と呼ばれるものの精神と深い利点からの逸脱であると私が考える理由の根本にあります。 Peter Landinは、より具体的に「denotative programming」と名付けました ここのコメントを参照してください
関数型プログラミングはラムダ計算から派生しています。機能的なプログラミングを本当に理解したい場合は、チェックアウトしてください http://worrydream.com/AlligatorEggs/
それはラムダ計算を学び、関数型プログラミングのエキサイティングな世界にあなたを連れて行く「楽しい」方法です!
Lambda Calculusを知ることは、関数型プログラミングにどのように役立ちますか。
したがって、ラムダ計算は、LISP、Scheme、ML、Haskellなどの多くの実際のプログラミング言語の基盤です。
入力に3を追加する関数を記述して、次のように記述したいとします。
_plus3 x = succ(succ(succ x))
_
「plus3は、任意の数xに適用されると、xの後継者の後継者を生み出す関数です」を読む
3を任意の数に加える関数はplus3という名前である必要はないことに注意してください。 「plus3」という名前は、この関数に名前を付けるのに便利な省略形です
(plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))
関数にはラムダ記号を使用していることに注意してください(これはアリゲーターのようなものだと思います。ワニの卵のアイデアの源はそれだと思います)
ラムダ記号はAlligator(関数)で、xはその色です。また、xを引数として考えることもできます(ラムダ計算関数は実際には引数が1つしかないと想定しています)。残りは関数の本体と考えることができます。
次に、抽象化について考えます。
_g ≡ λ f. (f (f (succ 0)))
_
引数fは、関数の位置(呼び出し)で使用されます。別の関数を入力として受け取るため、gを高次関数と呼びます。他の関数呼び出しfは "eggs"と考えることができます。これで、2つの関数または「Alligators」を取得して、次のようなことができます。
_(g plus3) = (λ f. (f (f (succ 0)))(λ x . (succ (succ (succ x))))
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
= ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
= (succ (succ (succ (succ (succ (succ (succ 0)))))))
_
気づいたら、λfアリゲーターがλxアリゲーターを食べ、次にλxアリゲーターを食べて死ぬことがわかります。次に、λxアリゲーターがλfのアリゲーターの卵に生まれ変わります。次に、このプロセスが繰り返され、左側のλxアリゲーターが右側の他のλxアリゲーターを食べます。
次に、「Alligators」が「Alligators "文法を設計することで、関数型プログラミング言語が誕生しました。
したがって、ラムダ計算を知っていれば、関数型言語の仕組みを理解できるでしょう。
Haskellで状態を処理する手法は非常に簡単です。そして、モナドを理解する必要はありません。
状態のあるプログラミング言語では、通常、どこかに値が格納され、コードが実行されてから、新しい値が格納されます。命令型言語では、この状態は「バックグラウンド」のどこかにあります。 (純粋な)関数型言語では、これを明示的にするため、状態を変換する関数を明示的に記述します。
したがって、タイプXのいくつかの状態を持つ代わりに、XをXにマップする関数を作成します。それだけです!状態について考えることから、その状態に対して実行したい操作について考えることに切り替えます。次に、これらの関数をチェーン化し、さまざまな方法で組み合わせて、プログラム全体を作成できます。もちろん、XをXにマッピングするだけに限定されません。データのさまざまな組み合わせを入力として受け取り、さまざまな組み合わせを最後に返す関数を作成できます。
モナドはこれを整理するのに役立つ多くのツールの1つです。しかし、モナドは実際には問題の解決策ではありません。解決策は、状態ではなく状態変換について考えることです。
これはI/Oでも機能します。実際に何が起こるかはこれです:scanf
に直接相当するものを使用してユーザーから入力を取得し、どこかに保存する代わりに、代わりにscanf
がある場合は、その関数をI/O APIに渡します。それがまさに>>=
は、HaskellでIO
モナドを使用するときに行います。したがって、I/Oの結果をどこにでも保存する必要はありません。変換方法を示すコードを記述するだけです。
(一部の関数型言語は不純な関数を許可します。)
完全に機能する言語の場合、実際の相互作用は通常、次のように関数の引数の1つとして含まれます。
RealWorld pureScanf(RealWorld world, const char* format, ...);
プログラマーから世界を抽象化するための言語は、言語によって異なります。たとえば、Haskellはモナドを使用してworld
引数を非表示にします。
しかし、関数型言語自体の純粋な部分はすでにチューリング完全です。つまり、Cで実行可能なことはすべてHaskellでも実行できます。命令型言語との主な違いは、適切な状態を変更することではありません。
int compute_sum_of_squares (int min, int max) {
int result = 0;
for (int i = min; i < max; ++ i)
result += i * i; // modify "result" in place
return result;
}
変更部分を関数呼び出しに組み込み、通常はループを再帰に変換します。
int compute_sum_of_squares (int min, int max) {
if (min >= max)
return 0;
else
return min * min + compute_sum_of_squares(min + 1, max);
}
関数型言語can状態を保存できます!彼らは通常、そうすることをあなたに明示するように奨励または強制するだけです。
たとえば、Haskellの State Monad を確認してください。
役に立つかもしれません 残りの人のための関数プログラミング
haskell:
main = do no <- readLn
print (no + 1)
もちろん、関数型言語の変数に物事を割り当てることができます。それらを変更することはできません(したがって、基本的にすべての変数は関数型言語の定数です)。