ステートレスプログラミングの利点?


131

私は最近関数型プログラミングについて学んでいます(特にHaskellですが、LispとErlangのチュートリアルも行ってきました)。概念が非常に啓発的であることがわかりましたが、「副作用なし」の概念の実際的な側面はまだわかりません。それの実際的な利点は何ですか?私は関数型の考え方で考えていますが、簡単な方法で状態を保存する機能がないと、非常に複雑に見える状況もあります(Haskellのモナドは「簡単」とは考えていません)。

Haskell(または他の純粋な関数型言語)を詳細に学習し続ける価値はありますか?関数型プログラミングまたはステートレスプログラミングは、手続き型プログラミングよりも実際に生産的ですか?Haskellや他の関数型言語を今後も使用する可能性はありますか、それとも理解のためだけに学習すべきですか?

私は生産性よりもパフォーマンスを気にしません。したがって、私は主に、手続き型/オブジェクト指向/その他よりも関数型言語でより生産的になるかどうかを尋ねています。

回答:


167

簡単な関数型プログラミングを読んでください。

ステートレスプログラミングには多くの利点があります。特に、劇的にマルチスレッド化された同時実行コードがその利点です。端的に言うと、可変状態はマルチスレッドコードの敵です。値がデフォルトで不変である場合、プログラマーは1つのスレッドが2つのスレッド間で共有状態の値を変更することを心配する必要がないため、競合状態に関連するマルチスレッドのバグのクラス全体が排除されます。競合状態がないため、ロックを使用する理由もないため、不変性によってデッドロックに関連する別のクラスのバグも排除されます。

それが関数型プログラミングが重要である大きな理由であり、おそらく関数型プログラミングトレインに飛び込むための最良の理由です。デバッグの簡素化(つまり、関数は純粋であり、アプリケーションの他の部分で状態を変化させない)、デザインパターンに大きく依存する言語に比べて簡潔で表現力豊かなコード、ボイラープレートコードが少ないなど、他にも多くの利点があります。コンパイラーは、コードをより積極的に最適化できます。


5
私はこれを2番目に!関数型プログラミングは、並列プログラミングに適しているため、今後さらに広く使用されると思います。
Ray Hidayat、

@Ray:分散プログラミングも追加します!
Anton Tykhyy、2009年

デバッグを除いて、そのほとんどは本当です。実際のコールスタックはなく、パターンマッチスタックしかないため、haskellでは一般的に難しくなります。そして、コードが最終的にどのようになるかを予測することははるかに困難です。
hasufell 2016年

3
また、関数型プログラミングは「ステートレス」ではありません。再帰はすでに暗黙の(ローカル)状態であり、haskellで行う主なことです。これは、慣用的なhaskellにいくつかの重要なアルゴリズム(たとえば、計算幾何学に関するもの)を実装し、それらのデバッグを楽しんだときに明らかになります。
Hasufell 2017年

2
FPでステートレスを同等化することは嫌いです。多くのFPプログラムは状態で満たされ、オブジェクトではなく単にクロージャーに存在します。
mikemaccana 2017年

46

プログラムのステートレスな部分が多いほど中断することなくピースを組み合わせる方法が増えます。ステートレスパラダイムの力は、それ自体がステートレス(または純粋性)にあるのではなく、強力で再利用可能な関数を記述してそれらを組み合わせることができる能力にあります。

John Hughesのペーパー「関数型プログラミングが重要な理由(PDF)」に、多くの例が含まれた優れたチュートリアルがあります。

あなたはなりゴブあなたはまた、代数的データ型とパターンマッチング(Camlの、SML、Haskellの)を持っていることを関数型言語を選ぶ場合は特に、より生産。


mixinもOOPと同様の方法で再利用可能なコードを提供しませんか?自分で物事を理解しようとするだけでOOPを提唱しない。
mikemaccana 2017年

20

他の答えの多くは、関数型プログラミングのパフォーマンス(並列処理)側に焦点を当てていますが、これは非常に重要であると思います。ただし、生産性について具体的に質問した場合と同様に、命令型パラダイムよりも機能パラダイムで同じことをより速くプログラミングできますか。

私は実際に(個人的な経験から)、F#でのプログラミングが私がよりよく考える方法と一致しているので、より簡単です。それが最大の違いだと思います。私はF#とC#の両方でプログラミングしましたが、F#の「言語との戦い」ははるかに少なく、大好きです。F#の詳細について考える必要はありません。これが私が実際に楽しんだもののいくつかの例です。

たとえば、F#は静的に型指定されていますが(すべての型はコンパイル時に解決されます)、型の推論により、どの型が使用されているかがわかるため、言う必要はありません。そして、それを理解できない場合、それは自動的にあなたの関数/クラス/何でも総称的にします。そのため、ジェネリックを記述する必要はありません。すべて自動です。それは、問題について考えるのにより多くの時間を費やしていて、それを実装する方法が少ないことを意味します。実際、C#に戻るたびに、この型の推論が本当に見当たらないことに気づきました。もうそれを行う必要がなくなるまで、それがどれほど煩わしいか気付くことはありません。

また、F#では、ループを作成する代わりに、関数を呼び出します。これは微妙な変更ですが、ループ構造についてもう考える必要がないため、重要です。たとえば、次のコードは、処理を実行して何かと一致します(何を思い出せないか、プロジェクトのオイラーパズルのコードです)。

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

C#でフィルターを実行してからマップ(各要素の変換)を実行するのは非常に簡単だと思いますが、より低いレベルで考える必要があります。特に、ループ自体を記述し、独自の明示的なifステートメント、およびそのようなことを行う必要があります。F#を習得して以来、機能的な方法でコーディングする方が簡単だと気づきました。フィルターしたい場合は、「フィルター」を記述し、マップしたい場合は、実装する代わりに「マップ」を記述します。詳細のそれぞれ。

また、F#とocamlを分離する|>演算子、およびおそらく他の関数型言語も気に入っています。これはパイプ演算子であり、ある式の出力を別の式の入力に「パイプ」することができます。それはコードが私がもっと考える方法に従うようにします。上記のコードスニペットと同様に、「因子シーケンスを取得し、フィルター処理してから、マッピングします」と言っています。これは非常に高いレベルの考え方であり、ループとifステートメントの記述で忙しいため、命令型プログラミング言語では理解できません。それは私が別の言語に行くときはいつも一番寂しいことです。

したがって、一般的に言えば、C#とF#の両方でプログラムを作成できますが、より高いレベルで考えることができるので、F#を使用する方が簡単です。(少なくともF#で)関数型プログラミングから細部が削除されているため、生産性が向上すると私は主張します。

編集:私は、関数型プログラミング言語での「状態」の例を要求したコメントの1つを見ました。F#は強制的に記述できるため、F#で変更可能な状態にする方法の直接的な例を次に示します。

let mutable x = 5
for i in 1..10 do
    x <- x + i

1
私はあなたの投稿に概ね同意しますが、|>は関数型プログラミング自体とは何の関係もありません。実際にa |> b (p1, p2)は、の構文糖質ですb (a, p1, p2)。これを右の連想性と組み合わせると、それがわかります。
アントンティクヒー、2009年

2
確かに、私がF#で得た経験の多くは、関数型プログラミングよりもF#に関係していることを認めるべきです。しかし、それでも、この2つの間には強い相関関係があり、型推論や|>などはそれ自体が関数型プログラミングではありませんが、確かに私はそれらが「領域にある」と主張します。少なくとも一般的には。
Ray Hidayat、

|>は、もう1つの高次中置関数であり、この場合は関数適用演算子です。独自の高階、中置演算子を定義することは、間違いなく関数型プログラミングの一部です(あなたがSchemerでない限り)。Haskellの$は同じですが、パイプラインの情報が右から左に流れます。
ノーマンラムジー

15

長い間デバッグに費やしてきたすべての困難なバグを検討してください。

さて、これらのバグのいくつは、プログラムの2つの別々のコンポーネント間の「意図しない相互作用」が原因でしたか?(ほぼすべてのスレッディングバグにはこの形式があります:共有データの書き込み、デッドロックなどの競合...さらに、グローバル状態に予期しない影響を与えるライブラリーを見つけたり、レジストリー/環境などを読み取り/書き込みしたりすることはよくあります。) I少なくとも3分の1の「ハードバグ」がこのカテゴリに分類されると考えられます。

ステートレス/不変/純粋なプログラミングに切り替えると、これらのバグはすべてなくなります。(あなたがするとき例えばあなたが代わりにいくつかの新たな課題が提示されないあなただけの種類を見ることができた手段、異なるモジュールは、環境と相互作用したい)が、Haskellのような言語では、それらの相互作用は、明示的に型システムに具体化されますプログラムの残りの部分との相互作用のタイプに関する機能と理由。

これが「不変性」IMOからの大きな勝利です。理想的な世界では、私たちはすべて素晴らしいAPIを設計し、状況が変更可能であっても、効果はローカルで十分に文書化され、「予期しない」相互作用は最小限に抑えられます。現実の世界では、無数の方法でグローバルな状態と相互作用する多くのAPIがあり、これらは最も悪質なバグの原因です。無国籍を目指すことは、コンポーネント間の意図しない/暗黙の/舞台裏の相互作用を取り除くことを目指しています。


6
誰かがかつて、変更可能な値を上書きすると、以前の値を明示的にガベージコレクション/解放していることになると言っていました。場合によっては、プログラムの他の部分はその値を使用して実行されませんでした。値を変更できない場合、このクラスのバグもなくなります。
2009年

8

ステートレス関数の1つの利点は、関数の戻り値の事前計算またはキャッシュが可能になることです。一部のCコンパイラでも、関数をステートレスとして明示的にマークして、最適化可能性を向上させることができます。他の多くの人が指摘したように、ステートレス関数は並列化がはるかに簡単です。

しかし、効率だけが問題ではありません。純粋な関数は、それに影響するものはすべて明示的に記述されているため、テストとデバッグが簡単です。また、関数型言語でプログラミングするときは、可能な限り「ダーティ」な関数(I / Oなど)を作成する習慣を身に付けます。このようにステートフルなものを分離することは、それほど機能的でない言語でも、プログラムを設計するための良い方法です。

関数型言語は「取得」に時間がかかる場合があり、そのプロセスを経験していない人に説明することは困難です。しかし、十分に長く存続するほとんどの人々は、関数型言語をあまり使用しない場合でも、やっとのことでそれだけの価値があることに気づきます。


その最初の部分は本当に興味深い点です、私は以前にそのことについて考えたことはありませんでした。ありがとう!
サーシャチェディゴフ2009年

あなたが持っていると仮定sin(PI/3)PIが一定であるあなたのコードで、コンパイラは、この機能を評価することができ、コンパイル時にして生成されたコードで結果を埋め込みます。
Artelius 2009年

6

状態がなければ、コードを自動的に並列化するのは非常に簡単です(CPUはますます多くのコアで作成されるため、これは非常に重要です)。


はい、私は間違いなくそれを調べました。特にErlangの並行性モデルは非常に興味深いものです。ただし、現時点では、同時実行性は生産性ほど重要ではありません。状態なしのプログラミングによる生産性のボーナスはありますか?
サーシャチェディゴフ2009年

2
@musicfreak、生産性のボーナスはありません。ただし、メモとして、最新のFP言語では、本当に必要な場合でもステートを使用できます。
不明

本当に?関数型言語で状態の例を挙げてもらえますか?そうすれば、それがどのように行われるかを確認できますか?
サーシャチェディゴフ2009年

- Haskellでの状態モナドをチェックbook.realworldhaskell.org/read/monads.html#x_NZ
ホタルブクロ

4
@不明:同意しません。状態なしでプログラミングすると、さまざまなコンポーネントの予期しない/意図しない相互作用が原因で発生するバグの発生が減少します。また、設計の改善も促進します(再利用性の向上、メカニズムとポリシーの分離など)。それは、目前のタスクに常に適切であるとは限りませんが、場合によっては本当に輝きます。
Artelius 2009年

6

トラフィックが増加し始めると、ステートレスWebアプリケーションが不可欠になります。

たとえばセキュリティ上の理由から、クライアント側に保存したくないユーザーデータがたくさんある場合があります。この場合、サーバー側に保存する必要があります。Webアプリケーションのデフォルトセッションを使用することもできますが、アプリケーションのインスタンスが複数ある場合は、各ユーザーが常に同じインスタンスにリダイレクトされるようにする必要があります。

多くの場合、ロードバランサーは、ユーザーのリクエストを送信するサーバーをロードバランサーが知っている「スティッキーセッション」を持つことができます。ただし、これは理想的ではありません。たとえば、Webアプリケーションを再起動するたびに、接続されているすべてのユーザーがセッションを失うことになります。

より良いアプローチは、セッションをWebサーバーの背後のある種のデータストアに格納することです。最近では、このために利用できる優れたnosql製品(redis、mongo、elasticsearch、memcached)がたくさんあります。このように、Webサーバーはステートレスですが、サーバー側の状態があり、この状態の可用性は、適切なデータストア設定を選択することで管理できます。これらのデータストアは通常、優れた冗長性を備えているため、ほとんどの場合、ユーザーに影響を与えることなく、Webアプリケーションやデータストアを変更することが可能です。


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