私は最近、関数型プログラミングについて学んでいます(具体的にはHaskellですが、LISPとErlangのチュートリアルも行っています)。概念は非常に啓発的であると感じましたが、「副作用なし」の概念の実際的な側面はまだありません。それの実際的な利点は何ですか?私は機能的な考え方で考えようとしていますが、簡単な方法で状態を保存する機能がないと、過度に複雑に見える状況がいくつかあります(Haskellのモナドは「簡単」だとは思いません)。
Haskell(または別の純粋に機能的な言語)を詳細に学習し続ける価値はありますか?関数型プログラミングまたはステートレスプログラミングは、実際には手続き型よりも生産的ですか?後でHaskellや他の関数型言語を使い続けるのでしょうか、それとも理解のためだけに学習すべきですか?
生産性よりもパフォーマンスを重視しています。だから私は主に、手続き型/オブジェクト指向/なんでもよりも関数型言語で生産的になるかどうかを尋ねています。
一言で言えば関数型プログラミング を読んでください。
ステートレスプログラミングには多くの利点がありますが、その多くは劇的にマルチスレッドおよび並行コードです。率直に言って、可変状態はマルチスレッドコードの敵です。値がデフォルトで不変である場合、プログラマーは、1つのスレッドが2つのスレッド間で共有状態の値を変更することを心配する必要はありません。競合状態がないため、ロックを使用する理由もないため、不変性はデッドロックに関連する別のクラスのバグも排除します。
それが関数型プログラミングが重要な大きな理由であり、おそらく関数型プログラミングトレインに飛び乗るのに最適な理由です。単純化されたデバッグ(つまり、関数は純粋でアプリケーションの他の部分の状態を変化させない)、設計パターンに大きく依存する言語に比べて簡潔で表現力豊かなコード、定型コードが少ないなど、他にも多くの利点があります。コンパイラはコードをより積極的に最適化できます。
プログラムのより多くの部分がステートレスです。何も壊さずに部分をまとめる方法が多くあります。ステートレスパラダイムの力は、ステートレス(または純粋さ)本来にあるのではなく、強力な再利用可能関数を記述し、それらを結合する能力です。
John Hughesの論文 Why Functional Programming Matters (PDF)で、多くの例を含む優れたチュートリアルを見つけることができます。
特に、代数的データ型とパターンマッチング(Caml、SML、Haskell)もある関数型言語を選択した場合は、gobsより生産的になります。
他の答えの多くは、関数型プログラミングのパフォーマンス(並列処理)側に焦点を当てています。ただし、生産性について具体的に尋ねました。たとえば、命令型パラダイムよりも機能的パラダイムで同じものをより速くプログラミングできますか。
私は実際に(個人的な経験から)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#を学習してから、機能的な方法でコーディングする方が簡単であることに気付きました。フィルタリングする場合は「filter」を記述し、マッピングする場合は「map」を記述する代わりに、各詳細。
また、F#をocamlや他の関数型言語から分離すると考えられる|>演算子も大好きです。これはパイプ演算子であり、ある式の出力を別の式の入力に「パイプ」できます。それは、コードが私がより思う方法に従うようにします。上記のコードスニペットのように、それは「因子シーケンスを取得し、それをフィルターに掛けてから、マッピングする」ということです。これは非常に高度な思考であり、ループやifステートメントの記述に忙しいため、命令型プログラミング言語では得られません。私が別の言語に行くとき、それは私が最も恋しく思う一つのことです。
したがって、一般的には、C#とF#の両方でプログラミングできますが、より高いレベルで考えることができるため、F#を使用する方が簡単です。機能プログラミング(少なくともF#では)から細かい部分が削除されているため、生産性が向上していると主張します。
編集:関数型プログラミング言語の「状態」の例を求めたコメントの1つを見ました。 F#は命令型で記述できるため、F#で可変状態にする方法の直接的な例を次に示します。
let mutable x = 5
for i in 1..10 do
x <- x + i
デバッグに長い時間を費やした困難なバグをすべて考慮してください。
さて、プログラムの2つの別個のコンポーネント間の「意図しない相互作用」が原因で、これらのバグはいくつありましたか? (ほぼすべてのスレッドバグには、この形式があります:共有データの書き込みに関連する競合、デッドロック、...さらに、グローバル状態に予期しない影響を与えるライブラリを見つけること、またはレジストリ/環境の読み取り/書き込みなど)- [〜#〜] i [〜#〜]は、少なくとも3分の1の「ハードバグ」がこのカテゴリに分類されると仮定します。
ステートレス/不変/純粋なプログラミングに切り替えると、これらのバグはすべてなくなります。代わりにいくつかの新しい課題が提示されます(たとえばdoが異なるモジュールに環境と対話する場合)が、Haskellのような言語では、それらの対話は明示的に型システムに具体化されます。関数のタイプと、プログラムの残りの部分との相互作用のタイプに関する理由を調べることができます。
これは「不変性」IMOからの大きな勝利です。理想的な世界では、すべてが素晴らしいAPIを設計し、物事が可変であったとしても、効果はローカルで十分に文書化され、「予期しない」相互作用は最小限に抑えられます。現実の世界には、無数の方法でグローバル状態と対話するAPIがたくさんあり、これらは最も有害なバグの原因です。ステートレスを目指すのは、コンポーネント間の意図しない/暗黙の/舞台裏の相互作用を取り除くことです。
ステートレス関数の利点の1つは、関数の戻り値の事前計算またはキャッシュを許可することです。一部のCコンパイラでも、関数をステートレスとして明示的にマークして、最適化を改善できます。他の多くの人が指摘しているように、ステートレス関数は並列化がはるかに簡単です。
しかし、効率だけが問題ではありません。純粋な関数は、影響を与えるものが明示的に記述されているため、テストとデバッグが簡単です。また、関数型言語でプログラミングする場合、「I/Oなどを使用して」「ダーティ」な関数をできるだけ少なくするという習慣があります。このようにステートフルなものを分離することは、あまり機能しない言語であっても、プログラムを設計するための良い方法です。
関数型言語は「取得」に時間がかかることがあり、そのプロセスを経ていない人に説明することは困難です。しかし、十分に長く存続するほとんどの人々は、関数型言語をあまり使用しなくても、大騒ぎはそれだけの価値があると最終的に認識します。
状態がなければ、コードを自動的に並列化するのは非常に簡単です(CPUはより多くのコアで作成されるため、これは非常に重要です)。
トラフィックを増やし始めるには、ステートレスWebアプリケーションが不可欠です。
たとえば、セキュリティ上の理由から、クライアント側に保存したくないユーザーデータがたくさんある可能性があります。この場合、サーバー側に保存する必要があります。 Webアプリケーションのデフォルトセッションを使用できますが、アプリケーションのインスタンスが複数ある場合は、各ユーザーが常に同じインスタンスにリダイレクトされるようにする必要があります。
多くの場合、ロードバランサーは、ユーザーリクエストを送信するサーバーをロードバランサーが何らかの方法で認識する「スティッキーセッション」を持つことができます。ただし、これは理想的ではありません。たとえば、Webアプリケーションを再起動するたびに、接続しているすべてのユーザーがセッションを失います。
より良いアプローチは、何らかの種類のデータストアのWebサーバーの背後にセッションを保存することです。最近では、これに使用できる優れたnosql製品(redis、mongo、elasticsearch、memcached)がたくさんあります。これにより、Webサーバーはステートレスになりますが、サーバー側の状態は維持され、適切なデータストア設定を選択することでこの状態の可用性を管理できます。通常、これらのデータストアには大きな冗長性があるため、ほとんどの場合、ユーザーに影響を与えることなくWebアプリケーションやデータストアに変更を加えることができます。
しばらく前に、このテーマに関する投稿を書きました: 純度の重要性について 。