web-dev-qa-db-ja.com

関数型プログラミングで副作用が悪と見なされるのはなぜですか?

副作用は自然現象だと思います。しかし、それは関数型言語のタブーのようなものです。理由は何ですか?

私の質問は、関数型プログラミングスタイルに固有です。すべてのプログラミング言語/パラダイムとは限りません。

70
Gulshan

副作用のない関数/メソッドを書く-純粋な関数なので、プログラムの正確さを簡単に推論できます。

また、これらの関数を簡単に作成して、新しい動作を作成することもできます。

また、コンパイラーが関数の結果をメモしたり、Common Subexpression Eliminationを使用したりできるなど、特定の最適化も可能になります。

編集:Benjolの要求に応じて:多くの状態がスタックに格納されるため(Jonasが呼び出したように、制御フローではなくデータフロー here )、これらのパーツの実行を並列化するか、別の方法で並べ替えることができます互いに独立した計算の1つのパーツが他のパーツへの入力を提供しないため、これらの独立したパーツを簡単に見つけることができます。

スタックをロールバックしてコンピューティングを再開できるデバッガー(Smalltalkなど)がある環境では、純粋な関数を使用すると、以前の状態を検査できるため、値の変化を非常に簡単に確認できます。変異の多い計算では、構造またはアルゴリズムに明示的に実行/取り消しアクションを追加しない限り、計算の履歴を表示できません。 (これは最初の段落に結びついています。純粋な関数を書くと、プログラムの正確さを検査するのが簡単になります。)

73
Frank Shearar

誤解しているかもしれませんが、関数型プログラミングは副作用を制限してプログラムを理解し最適化しやすくします。 Haskellでもファイルへの書き込みが可能です。

基本的に私が言っているのは、関数型プログラマーは副作用を悪とは考えておらず、副作用の使用を制限することは良いことだと考えているということです。私はそれがそのような単純な区別のように見えるかもしれないことを知っていますが、それはすべての違いを生みます。

24
ChaosPandion

関数型プログラミング に関する記事から:

実際には、アプリケーションにはいくつかの副作用が必要です。関数型プログラミング言語Haskellの主要な貢献者であるSimon Peyton-Jones氏は、次のように述べています。箱が熱くなっていること」 ( http://oscon.blip.tv/file/324976 )重要なのは、副作用を制限し、明確に識別し、コード全体に散らばらないようにすることです。

23
Peter Stuifzand

いくつかのメモ:

  • 副作用のない関数は簡単に並列実行できますが、副作用のある関数は通常、なんらかの同期が必要です。

  • 副作用のない関数は、より積極的な最適化を可能にします(たとえば、結果キャッシュを透過的に使用することにより)。正しい結果が得られる限り、関数がreallyであったかどうかは問題ではないためです。実行された

13
user281377

現在私は主に関数型コードで作業していますが、その観点からすれば、それは盲目的に明白なようです。副作用は、コードを読んで理解しようとするプログラマーにhugeの精神的負担をもたらします。しばらくの間それから解放されるまでその負担に気付かず、突然、副作用のあるコードを再度読み取る必要があります。

次の簡単な例を考えてみましょう。

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

関数型言語では、私はfooがまだ42であることを知っています。私はlook中間のコードを理解するか、コードを理解するか、コードが呼び出す関数の実装を確認します。

同時実行性、並列化、最適化に関することはすべてすばらしいですが、それはコンピュータ科学者がパンフレットに載せたものです。変数を変更しているのは誰で、いつ私が実際に楽しんでいるのかを不思議に思う必要はありません。

11
Karl Bielefeldt

副作用を引き起こすことを不可能にする言語はほとんどありません。完全に副作用がない言語は、容量が非常に限られている場合を除いて、使用するのが非常に困難(ほぼ不可能)になります。

副作用はなぜ悪と見なされるのですか?

それは、プログラムが何をするのかを正確に推論し、プログラムが期待どおりに動作することを証明することをはるかに難しくするからです。

非常に高いレベルで、ブラックボックステストのみで3層Webサイト全体をテストすることを想像してみてください。確かに、規模によっては可能です。しかし、確かに多くの重複が起こっています。そして、バグ(副作用に関連する)がある場合、バグが診断および修正されるまで、システム全体を破壊して、さらにテストすることができます。修正はテスト環境にデプロイされます。

メリット

それを縮小します。副作用のないコードを書くことにかなり長けているとしたら、既存のコードが何をしたのかを推論するのにどれだけ速くなりますか?単体テストをどれだけ速く書けますか?副作用のないコードがバグのないことが保証され、ユーザーがコードdidのバグへの露出を制限できるとどの程度自信がありますか?

コードに副作用がない場合、コンパイラーは実行できる追加の最適化も行う場合があります。これらの最適化を実装する方がはるかに簡単な場合があります。副作用のないコードの最適化を概念化する方がはるかに簡単です。つまり、コンパイラベンダーが、副作用のあるコードでは不可能である最適化を実装する可能性があります。

並行性は、コードに副作用がない場合の実装、自動生成、最適化も大幅に簡単です。これは、すべてのピースを任意の順序で安全に評価できるためです。プログラマが高度な並行コードを記述できるようにすることは、コンピュータサイエンスが取り組む必要のある次の大きな課題であり、 ムーアの法則 に対する数少ないヘッジの1つであると広く考えられています。

副作用は、コード内の「リーク」のようなものであり、後で、または疑いを持たない同僚が処理する必要があります。

関数型言語は、コードのコンテキスト依存性を減らし、モジュール性を高める方法として、状態変数と可変データを回避します。モジュール性は、ある開発者の作業が別の開発者の作業に影響を与えたり、弱めたりしないことを保証します。

開発率をチームの規模に合わせてスケーリングすることは、今日のソフトウェア開発の「聖杯」です。他のプログラマーと作業する場合、モジュール性ほど重要なものはほとんどありません。最も単純な論理的副作用でさえ、コラボレーションを非常に困難にします。

4
Ami

まあ、私見、これはかなり偽善的です。誰も副作用を好みませんが、誰もがそれを必要とします。

副作用について非常に危険なのは、関数を呼び出すと、次に呼び出されたときの関数の動作だけでなく、他の関数にも影響を与える可能性があることです。したがって、副作用は予測できない動作と重要な依存関係をもたらします。

OOやfunctionなどのプログラミングパラダイムは、この問題に対処します。OOは、懸念事項を分離することにより問題を軽減します。これは、多くの変更可能なデータはオブジェクトにカプセル化され、それぞれが自身の状態のみを維持する役割を果たします。これにより、依存関係のリスクが軽減され、問題がはるかに分離され、追跡が容易になります。

関数型プログラミングははるかに根本的なアプローチを採用しており、アプリケーションの状態はプログラマーの観点からは単純に不変です。これはいい考えですが、それだけでは言語が役に立たなくなります。どうして?どんなI/O操作にも副作用があるからです。入力ストリームから読み取るとすぐにアプリケーションの状態が変化する可能性があります。次回同じ関数を呼び出したときに、結果が異なる可能性があるためです。別のデータを読み取っている可能性があります。または、可能性として、操作が失敗する可能性もあります。出力についても同様です。出力も副作用のある演算です。これは、今日よく理解していることではありませんが、出力に20Kしかないと想像してください。それ以上出力すると、ディスク容量が足りなくなったなどの理由でアプリがクラッシュします。

ですから、プログラマーの観点からすると、副作用は厄介で危険です。ほとんどのバグは、アプリケーションの状態の特定の部分が、ほとんど考慮されていない、多くの場合不必要な副作用によって、ほとんどあいまいな方法でインターロックされていることに起因しています。ユーザーの観点からすると、副作用はコンピュータを使用するポイントです。彼らは内部で何が起こるか、それがどのように組織化されているかを気にしません。彼らは何かをし、それに応じてコンピュータが変化することを期待します。

4
back2dos

副作用があると、テスト時に考慮に入れなければならない追加の入出力パラメーターが導入されます。

環境は検証対象のコードだけに限定できないため、コードの検証がはるかに複雑になりますが、周囲の環境の一部またはすべてを取り込む必要があります(更新されるグローバルは、そのコードの向こう側に存在します。コードは、完全なJava EEサーバー....)

副作用を回避しようとすることで、コードの実行に必要な外部性の量を制限します。

2
user1249

私の経験では、オブジェクト指向プログラミングの優れた設計では、副作用のある関数の使用が義務付けられています。

たとえば、基本的なUIデスクトップアプリケーションを見てみましょう。私のプログラムのドメインモデルの現在の状態を表すオブジェクトグラフをヒープ上に持つ実行中のプログラムがあるかもしれません。メッセージがそのグラフのオブジェクトに到着します(たとえば、UIレイヤーコントローラーから呼び出されたメソッド呼び出しを介して)。ヒープ上のオブジェクトグラフ(ドメインモデル)は、メッセージに応じて変更されます。モデルのオブザーバーには変更が通知され、UIや他のリソースが変更される可能性があります。

悪であるどころか、これらのヒープ変更および画面変更の副作用の正しい配置は、OO設計(この場合はMVCパターン))の中核にあります。

もちろん、それはあなたのメソッドが任意の副作用を持つべきだという意味ではありません。また、副作用のない関数は、コードの読みやすさを向上させ、場合によってはパフォーマンスを向上させます。

1
flamingpenguin

上記の質問が指摘したように、関数型言語はコードが副作用を持つことをそれほど防止しないので、副作用を管理するためのツールを提供する特定のコードでいつ発生するか。

これは非常に興味深い結果をもたらすことがわかりました。まず、そして最も明らかなことですが、副作用のないコードを使用してできることは数多くあります。しかし、副作用のあるコードを操作する場合でも、他にもできることがいくつかあります。

  • 変更可能な状態のコードでは、特定の関数の外にリークしないように状態のスコープを静的に管理できるため、参照カウントやマークアンドスイープスタイルスキームなしでガベージを収集できます。 、まだ参照が存続しないことを確認してください。同じ保証は、プライバシーに敏感な情報などを維持するためにも役立ちます(これは、haskellでSTモナドを使用して実現できます)。
  • 複数のスレッドで共有状態を変更する場合、変更を追跡し、トランザクションの最後にアトミック更新を実行するか、別のスレッドが競合する変更を行った場合にトランザクションをロールバックして繰り返すことにより、ロックの必要性を回避できます。これは、コードが状態の変更(喜んで放棄することができる)以外に影響を及ぼさないことを保証できるためにのみ実現可能です。これは、HaskellのSTM(Software Transactional Memory)モナドによって実行されます。
  • コードの効果を追跡し、それを簡単にサンドボックス化して、安全であることを確認するために実行する必要があるすべての効果をフィルタリングし、(たとえば) ユーザーが入力したコードをWebサイトで安全に実行できるようにする)
0
Jules

悪は少し上にあります。それはすべて、言語の使用状況に依存します。

すでに述べたものに対する別の考慮事項は、機能的な副作用がない場合、プログラムの正当性の証明をはるかに簡単にすることです。

0
Ilan

複雑なコードベースでは、副作用の複雑な相互作用は、私が推論するのが最も難しいことです。自分の脳の働きを考えれば、個人的にしか話せません。副作用、持続的な状態、変化する入力などによって、個々の関数で「何が」起こっているのかだけでなく、「いつ」「どこで」何が起こっているのかを正当性について推論する必要があります。

私は「何に」に集中することはできません。関数を徹底的にテストした後、それを使用してコード全体に信頼性の空気を広げる関数を徹底的にテストすることはできません。呼び出し側は、間違ったタイミングで、間違ったスレッドから、間違って呼び出して、誤用する可能性があるためです。注文。一方、副作用を引き起こさず、入力を(入力に触れずに)指定した新しい出力を返すだけの関数は、この方法で誤用することはほとんど不可能です。

しかし、私は実用的なタイプだと思います、または少なくともそうしようとします。そして、コードの正確さを推論するために、すべての副作用を最低限に打ち抜く必要があるとは限りません(少なくともこれをCなどの言語で行うのは非常に難しいと思います。正確さについて推論するのが非常に難しいのは、複雑な制御フローと副作用の組み合わせがある場合です。

私にとって複雑な制御フローは、本質的にグラフのようなもので、多くの場合、再帰的または再帰的です(イベントキュー、たとえば、イベントを直接再帰的に呼び出していませんが、本質的に「再帰的」です)。実際のリンクされたグラフ構造をトラバースするプロセス、またはイベントの折衷的な混合を含む不均一なイベントキューを処理するプロセスで、コードベースのすべての種類のさまざまな部分とすべての異なる副作用のトリガーにつながります。最終的にコード内のすべての場所を引き出そうとすると、それは複雑なグラフに似ており、グラフ内のノードがその瞬間にそこにあるとは予想もしていなかったかもしれません。副作用を引き起こします。つまり、呼び出されている関数だけでなく、その間に発生している副作用とその発生順序にも驚かれる可能性があります。

関数型言語は非常に複雑で再帰的な制御フローを持つ可能性がありますが、その過程で折衷的な副作用が発生するわけではないため、結果は正確さの点で非常に簡単に理解できます。複雑な制御フローが折衷的な副作用に出会ったときだけで、何が起こっているのか、そして常に正しいことを常に行うかどうかを理解しようとすることは頭痛の種です。

そのため、これらのケースがある場合、予期せぬ事態に陥ることなくコードに変更を加えることができると確信しているだけでなく、そのようなコードの正確さに非常に自信を持つことは、不可能ではないにしても非常に難しいことがよくあります。したがって、私に対する解決策は、制御フローを単純化するか、副作用を最小化/統合することです(統合することで、システムの特定のフェーズで2種類または3種類ではなく、1種類の副作用のみを多くのことに引き起こすようなものですダース)。存在するコードの正しさと、導入する変更の正しさについて、私のシンプルトンの脳が自信を持てるようにするには、これら2つのことの1つが必要です。次のように、制御フローに沿って副作用が均一かつ単純である場合、副作用を導入するコードの正確さについて自信を持つのは非常に簡単です。

for each pixel in an image:
    make it red

このようなコードの正確さを推測するのは簡単ですが、主に副作用が非常に均一で、制御フローが非常に単純であるためです。しかし、次のようなコードがあるとします。

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove Edge
         remove vertex

次に、これは途方もなく過度に単純化された疑似コードであり、通常ははるかに多くの関数とネストされたループ、および続行する必要があるはるかに多くのものが含まれます(複数のテクスチャマップ、ボーンウェイト、選択状態などの更新)。複雑なグラフのような制御フローと進行中の副作用の相互作用による正確さの理由。したがって、これを簡略化する1つの戦略は、処理を延期し、一度に1つのタイプの副作用にのみ焦点を当てることです。

for each vertex to remove:
     mark connected edges
for each marked Edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked Edge:
     remove Edge
for each vertex to remove:
     remove vertex

...単純化の1つの反復としてこの効果に何か。つまり、データを複数回通過することになるため、計算コストが確実に発生しますが、そのような結果のコードをより簡単にマルチスレッド化できることがよくあります。これは、副作用と制御フローがこの均一で単純な性質を帯びているためです。さらに、各ループは、接続されたグラフをたどって副作用を引き起こすよりもキャッシュフレンドリーにすることができます(たとえば、並列ビットセットを使用して、トラバースする必要があるものにマークを付けて、並べ替えられた順序で遅延パスを実行できるようにしますビットマスクとFFSを使用)。しかし、最も重要なのは、2番目のバージョンの方が、バグを発生させることなく、正確さと変更の点で非常に簡単に推論できることです。それが私がとにかくそれにアプローチする方法であり、イベント処理の単純化など、上記のメッシュ処理を単純化するために同じ種類の考え方を適用します-均一な副作用を引き起こすデッドシンプルな制御フローを持つより均質なループ。

そして結局のところ、ある時点で副作用が発生する必要があります。そうしないと、どこにも行かずにデータを出力する関数ができてしまいます。多くの場合、何かをファイルに記録したり、画面に表示したり、ソケットを介してデータを送信したりする必要があります。これらはすべて副作用です。しかし、発生する余分な副作用の数を確実に減らすことができ、制御フローが非常に複雑な場合に発生する副作用の数も減らすことができます。そうすれば、バグを回避する方がはるかに簡単になると思います。

0
user204677