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


69

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

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


6
副作用のないプログラムは役に立たないので、副作用は邪悪でもタブーでもありません。しかし、FPは副作用を伴うコードの区切りを強制するため、可能な限りコードの大部分は副作用のない関数です。これは、副作用のない関数とサブシステムが理解しやすく、分析しやすく、テストしやすく、最適化しやすいため、推奨されます。
ジャックB

@JacquesBなぜ理解しやすく、分析しやすく、テストしやすく、最適化しやすいのかを説明するのは良い答えになるでしょう。
16

回答:


72

副作用のない関数/メソッドを記述することで、純粋な関数になります -プログラムの正確さについて推論するのが容易になります。

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

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

編集:Benjolの要求:状態の多くはスタックに保存されているため(Jonasがここで呼び出したように、制御フローではなくデータフロー)、独立した計算部分の実行を並列化または再順序付けできますお互い。1つのパーツが他のパーツに入力を提供しないため、これらの独立したパーツを簡単に見つけることができます。

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


4
あなたの答えに並行性について何かを追加することを検討してください?
Benjol

5
副作用のない関数は、テストと再利用が簡単です。
LennyProgrammers

@ Lenny222:再利用は、関数の構成について話すことで私がほのめかしていたことでした。
フランクシェラー

@フランク:ああ、わかりました、あまりにも浅いブラウジング。:)
LennyProgrammers

@ Lenny222:大丈夫です。それを綴るのはおそらく良いことです。
フランクシェラー

23

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

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



23

間違っていますが、関数型プログラミングは副作用の制限を促進し、プログラムの理解と最適化を容易にします。Haskellでさえ、ファイルに書き込むことができます。

基本的に、私が言っているのは、機能的なプログラマーは副作用を悪とは思わず、単に副作用の使用を制限することは良いと思うということです。私はそれがそのような単純な区別のように見えるかもしれないことを知っているが、それはすべての違いを生む。


それが「タブーのようなもの」である理由です-FPLは副作用を制限することをお勧めします。
フランクシェラー

アプローチのために+1。副作用はまだ存在します。確かに、それらは限られています
ベラン

明確にするために、「関数型プログラミングで副作用が許可されない理由」や「副作用が不要な理由」は述べていません。私はそれが関数型言語で許可されていることを知っています。しかし、関数型プログラミングでは非常に推奨されていません。どうして?それが私の質問でした。
グルシャン

@Gulshan-副作用により、プログラムの理解と最適化が難しくなります。
ChaosPandion

haskellの場合、主なポイントは「副作用を制限する」ことではありません。副作用は、言語で表現することは不可能です。機能のようなものreadFileは、アクションのシーケンスを定義しています。このシーケンスは機能的に純粋で、何をすべきかを説明する抽象的なツリーのようなものです。その後、実際のダーティな副作用がランタイムによって実行されます。
サラ

13

いくつかのメモ:

  • 副作用のない関数は、ささいなことに並行して実行できますが、副作用のある関数は通常、何らかの同期を必要とします。

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


非常に興味深い点:関数が実際に実行されたかどうかは関係ありません。同等のパラメーターが与えられた場合に、副作用のない関数への後続の呼び出しを取り除くことができるコンパイラーになることは興味深いでしょう。
ノエルウィドマー

1
@NoelWidmerそのようなものはすでに存在します。OracleのPL / SQLは、deterministic副作用のない関数の句を提供するため、必要以上に頻繁に実行されることはありません。
user281377

うわー!ただし、コンパイラーが明示的なフラグを指定せずにそれ自体でそれを理解できるように、言語は意味的に表現力があるべきだと思います(句が何であるかはわかりません)。解決策は、パラメーターを可変/不変feに指定することです。一般的に言えば、これはコンパイラーによる副作用についての仮定ができる強力な型システムを必要とします。また、必要に応じてこの機能をオフにする必要があります。オプトインではなくオプトアウト。それは私の意見です、あなたの答えを読んだ後、私が持っている限られた知識に基づいています:)
ノエル・ウィドマー

このdeterministic節は、これが決定論的な関数であることをコンパイラーに伝える単なるfinalキーワードであり、Java のキーワードが変数を変更できないことをコンパイラーに伝える方法に匹敵します。
user281377

11

私は現在、主に機能的なコードで作業しており、その観点からは目がくらむほど明白です。副作用は、コードを読んで理解しようとするプログラマーに大きな精神的負担をかけます。あなたはしばらくの間それから解放されるまでその負担に気づかず、突然副作用のあるコードを再び読む必要があります。

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

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?

関数型言語では、それがまだ42であることを知っています。その間のコードfooたり、あまり理解したり、呼び出す関数の実装を見たりする必要さえありません。

並行性と並列化と最適化に関することはすべて素晴らしいことですが、それはコンピューター科学者がパンフレットに載せたものです。誰があなたの変数を変化させているのか不思議に思う必要はありません。いつ練習するのが本当に好きなのです。


6

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

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

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

非常に高いレベルで、ブラックボックステストのみで3層のWebサイト全体をテストすることを想像してください。もちろん、規模に応じて実行可能です。しかし、確かに多くの重複が起こっています。あればあるバグが(それが副作用に関連している)バグが診断され、固定され、修正がテスト環境にデプロイされるまで、あなたは潜在的に、更なる試験のために、システム全体を破ることができました。

利点

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

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

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


1
Adaは副作用を引き起こすことを非常に困難にします。それは不可能ではありませんが、あなたはそのとき何をするかを明確に知っています。
mouviciel

@mouviciel:副作用を非常に難しくし、それらをMonadsに委ねようとする有用な言語が少なくともいくつかあると思います。
マーリンモーガングラハム

4

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

関数型言語は、コードをコンテキストに依存せず、よりモジュール化する方法として、状態変数と可変データを回避します。モジュール性により、ある開発者の作業が別の開発者の作業に影響を与えたり、損なわれたりすることがなくなります。

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


+
1-

1
副作用が「処理する必要があるリーク」の場合は-1。「副作用」(純粋に機能しないコード)を作成することは、重要なコンピュータープログラムを作成する目的のすべてです。
メイソンウィーラー

このコメントは6年後に来ますが、副作用があり、それから副作用があります。ユーザーにあなたの結果を与える必要があるため、副作用の望ましい種類なので、上のI / Oを行っているとは、実際には任意のプログラムに必要な何らかの形ではなく、あなたのコードが変化したときの副作用の他の種類、 - なしで良いですI / Oを行うなどの理由は、実際に後で処理する必要がある「リーク」です。基本的な考え方はコマンドとクエリの分離です。値を返す関数には副作用がありません。
rmunn

4

まあ、私見、これは非常に偽善的です。副作用が好きな人はいませんが、誰もがそれを必要としています。

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

オブジェクト指向や関数型などのプログラミングパラダイムは両方ともこの問題に対処します。OOは、関心の分離を課すことで問題を軽減します。これは、多くの可変データで構成されるアプリケーションの状態がオブジェクトにカプセル化されることを意味します。各オブジェクトは、独自の状態のみを維持する責任があります。これにより、依存関係のリスクが軽減され、問題がはるかに分離され、追跡しやすくなります。

関数型プログラミングは、プログラマーの観点からはアプリケーションの状態が単純に不変である、はるかに過激なアプローチを取ります。これはいい考えですが、言語自体を役に立たないものにします。どうして?すべてのI / O操作には副作用があるためです。入力ストリームから読み取るとすぐに、アプリケーションの状態が変わる可能性があります。同じ関数を次に呼び出すと、結果が異なる可能性が高いためです。異なるデータを読み込んでいるか、または可能性として-操作が失敗する可能性があります。出力についても同様です。偶数出力は副作用のある操作です。これは最近よく気づいていることではありませんが、出力に20Kしかないことを想像してください。それ以上出力すると、ディスク容量が足りないなどの理由でアプリがクラッシュします。

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


興味深いことに、論理プログラミングには機能的な副作用がないだけではありません。ただし、割り当てられた変数の値を変更することもできません。
イラン

@Ilan:これは一部の関数型言語にも当てはまり、採用しやすいスタイルです。
back2dos

「関数型プログラミングは、アプリケーションの状態がプログラマーの観点から単純に不変である、はるかに急進的なアプローチを取ります。これは素晴らしいアイデアですが、言語自体を役に立たなくします。理由は何ですか?効果」:FPは副作用を禁止しません。むしろ、必要でない場合は副作用を制限します。例(1)I / O->副作用が必要です。(2)値のシーケンスから集計関数を計算する->副作用(たとえば、アキュムレータ変数を使用したループ)は不要です。
ジョルジオ

2

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

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

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


1

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

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

邪悪ではなく、これらのヒープ変更およびスクリーン変更の副作用の正しい配置は、OOデザイン(この場合はMVCパターン)の中核にあります。

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


1
オブザーバー(UIを含む)は、オブジェクトが送信するメッセージ/イベントをサブスクライブすることにより、変更について知る必要があります。オブジェクトがオブザーバーを直接変更しない限り、これは副作用ではありません-これは設計が悪いでしょう。
ChrisF

1
@ChrisF最も間違いなく、それは副作用です。オブザーバーに渡されるメッセージ(オブジェクト指向言語では、インターフェイスでのメソッド呼び出しが最も可能性が高い)により、ヒープ上のUIコンポーネントの状態が変化します(これらのヒープオブジェクトはプログラムの他の部分に表示されます)。UIコンポーネントは、メソッドのパラメーターでも戻り値でもありません。正式な意味では、関数に副作用がないためには、べき等でなければなりません。MVCパターンの通知はそうではありません。たとえば、UIは受け取ったメッセージのリストを表示することがあります-コンソール-2回呼び出すと、異なるプログラム状態になります。
flamingpenguin

0

悪はちょっと上です。それはすべて、言語の使用状況に依存します。

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


0

上記の質問が指摘したように、関数型言語は、特定のコードでいつ副作用が発生するかを管理するためのツールを提供するのでコードが副作用を防ぐことはあまりありません。

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

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

0

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

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

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

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

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

そのため、これらのケースがある場合、そのようなコードの正確性について非常に自信を持って感じることは不可能ではないにしても、非常に困難であることがよくあります。したがって、私に対する解決策は、制御フローを簡素化するか、副作用を最小化/統合することです(統合することにより、システムの特定の段階で多くの事柄に1種類の副作用のみを引き起こし、2つまたは3つまたはダース)。存在するコードの正確性と導入する変更の正確性について、simpleton brainが自信を持てるようにするために、これら2つのことのいずれかが必要です。副作用が制御フローとともに均一で単純な場合、副作用の原因となるコードの正確性について自信を持つことは非常に簡単です。

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番目のバージョンのほうがはるかに簡単に推論できることです。そのため'

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


-1

それは悪ではありません。私の意見では、副作用がある場合とない場合の2つの関数タイプを区別する必要があります。副作用のない関数:-同じ引数で常に同じを返すため、たとえば引数のないこのような関数は意味がありません。-それはまた、そのようないくつかの関数が呼び出される順序は何の役割も果たさないことを意味します-他のコードなしで、単独で(!)実行でき、デバッグできる必要があります。そして今、笑、JUnitが作るものを見てください。副作用のある関数:-一種の「リーク」、自動的に強調表示できるもの-デバッグと間違いの検索により、一般的に副作用によって引き起こされるものが非常に重要です。-副作用のある機能には、副作用のない「部分」もあり、自動的に分離することもできます。これらの副作用は悪です


ポイントが作られ、前12件の答えで説明した上で、これはかなりのものを提供していないようだ
ブヨ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.