質問

副作用は自然現象だと感じています。しかし、それは機能的言語のタブーのようなものです。理由は何ですか?

私の質問は機能的なプログラミングスタイルに固有です。すべてのプログラミング言語/パラダイムではありません。

役に立ちましたか?

解決

副作用なしにあなたの機能/方法を書く - だから彼らは 純粋な機能 - プログラムの正しさについて推論しやすくなります。

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

また、コンパイラが機能の結果をメモ化するか、共通のサブエクスペッション除去を使用できる場合、特定の最適化を可能にします。

編集:ベンジョルの要求で:あなたの州の多くがスタックに保存されているため(データフロー、コントロールフローではなく、ジョナスがそれを呼んだように ここ)、互いに独立した計算の部分の実行を並列化または並べ替えることができます。 1つの部品が他の部分に入力を提供しないため、これらの独立した部分を簡単に見つけることができます。

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

他のヒント

についての記事から 機能プログラミング:

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

あなたはそれを間違えており、機能的なプログラミングは、プログラムを理解し、最適化しやすくするために制限副作用を促進します。 Haskellでさえ、ファイルに書き込むことができます。

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

いくつかのメモ:

  • 副作用のない関数は些細なものを並行して実行できますが、副作用を持つ関数は通常、何らかの同期を必要とします。

  • 副作用のない機能により、より積極的な最適化が可能になります(例:結果キャッシュを使用して透明にすることによる)。正しい結果が得られる限り、関数があったかどうかは関係ないからです 本当 実行された

私は主に機能コードで働いていますが、その観点からは盲目的に明白に思えます。副作用を作成します 巨大 コードを読み、理解しようとしているプログラマーの精神的負担。しばらくの間自由になるまで、その負担に気付かないので、突然副作用を伴うコードを再び読む必要があります。

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

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?

機能的な言語では、i 知る それ foo まだ42です。私もそうする必要さえありません 見る その間のコードでは、それがあまり理解されていないか、それが呼び出す関数の実装を調べます。

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

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

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

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

非常に高いレベルで、ブラックボックステストのみで3層のWebサイト全体をテストすることを想像してください。確かに、スケールに応じて実行可能です。しかし、確かに多くの重複が続いています。そしてそこにあれば バグ(副作用に関連する)、バグが診断されて固定され、修正がテスト環境に展開されるまで、さらにテストするためにシステム全体を破壊する可能性があります。

利点

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

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

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

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

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

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

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

副作用について非常に危険なのは、関数を呼び出すと、次回呼び出されたときに機能が動作する方法だけでなく、他の機能にこの効果がある場合だけでなく、これが効果をもたらす可能性があることです。したがって、副作用は、予測不可能な行動と非自明の依存関係を導入します。

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

機能プログラミングは、アプリケーション状態がプログラマーの観点から単に不変であるはるかに急進的なアプローチを取ります。これはいいアイデアですが、それ自体で言語を役に立たないものにします。なんで? I/O-Operationには副作用があるためです。入力ストリームから読むとすぐに、アプリケーション状態が変更される可能性があります。次回同じ関数を呼び出すと、結果が異なる可能性が高いためです。あなたは異なるデータを読んでいるかもしれません、または - 可能性も - 操作が失敗する可能性があります。同じことが出力にも当てはまります。偶数出力は、副作用を備えた操作です。これは最近頻繁に気づくことは何もありませんが、出力に20kしかないと想像してください。これ以上出力が出力されれば、ディスクスペースなどがないためにアプリがクラッシュします。

したがって、はい、副作用はプログラマーの観点からは厄介で危険です。ほとんどのバグは、アプリケーション状態の特定の部分が、意図せず、しばしば不必要な副作用を通じて、ほぼ不明瞭な方法で連動する方法から来ます。ユーザーの観点から見ると、副作用はコンピューターを使用するポイントです。彼らは、内部で何が起こるか、それがどのように組織されているかを気にしません。彼らは何かをし、それに応じてコンピューターが変わることを期待しています。

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

これにより、環境が検証されているだけでなく、周囲の環境の一部またはすべてをもたらす必要があるため、コード検証がはるかに複雑になります(そのコードの存続状態にあるグローバルは、それがそれに依存します。コードは、完全なJava EEサーバー内での生活に依存します。)

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

私の経験では、オブジェクト指向プログラミングの優れたデザインは、副作用を持つ機能の使用を義務付けています。

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

悪とはほど遠く、これらのヒープ修正および画面修飾の副作用の正しい配置は、OO設計の中核にあります(この場合はMVCパターン)。

もちろん、それはあなたの方法が幼稚園の副作用を持つべきであるという意味ではありません。また、副作用フリー機能には、コードの読み取りバリティとパフォーマンスを改善する場所があります。

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

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

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

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

  • 可変状態を持つコードでは、特定の関数の外側に漏れることができないことを静的に保証するような状態の範囲を管理できます。これにより、参照カウントまたはマークアンドスイープスタイルのスキームなしでゴミを収集できます。 、まだ参照が生き残れないことを確認してください。同じ保証は、プライバシーに敏感な情報などの維持にも役立ちます(これは、HaskellのSt Monadを使用して達成できます)
  • 共有状態を複数のスレッドで変更する場合、変更を追跡してトランザクションの終了時にアトミックアップデートを実行するか、トランザクションをバックバックして繰り返して、別のスレッドが競合する変更を加えた場合に繰り返すことにより、ロックの必要性を回避できます。これは、コードに州の変更以外の効果がないことを保証できるため、実現可能です(喜んで放棄することができます)。これは、HaskellのSTM(ソフトウェアトランザクションメモリ)Monadによって実行されます。
  • コードの効果を追跡し、それを簡単にサンドボックスし、安全であることを確認するために実行する必要がある効果をフィルタリングし、(たとえば)許可することができます。 Webサイトで安全に実行されるユーザー入力コード

複雑なコードベースでは、副作用の複雑な相互作用は、私が推論するのが最も難しいことです。脳の仕組みを考えると、個人的に話すことができます。副作用と永続的な状態、および突然変わりの入力など、個々の関数で起こっていることだけでなく、「いつ」と「どこで」が正しいことを推論するのかを考えなければなりません。

「何」に集中することはできません。副作用を引き起こす関数を徹底的にテストした後、コードを使用してコード全体に信頼性の空気を広めることを徹底的にテストすることはできません。注文。一方、副作用を引き起こさず、入力を(入力に触れずに)与えられた新しい出力を返すだけの関数は、この方法で誤用することはほとんど不可能です。

しかし、私は実用的なタイプだと思います。 c)のような言語でこれを行うのは非常に難しいと思います。私が正確性について推論するのが非常に難しいと思うのは、複雑な制御フローと副作用の組み合わせがあるときです。

私への複雑な制御の流れは、自然の中でグラフのようなものであり、しばしば再帰的または再帰的なものです(例えば、イベントのキュー、それは直接再帰的に呼び出すのではなく、本質的に「再帰的な」)、おそらく物事をする実際のリンクされたグラフ構造を横断するプロセス、またはイベントの折lect的な混合物を含む非多数のイベントキューを処理して、コードベースのあらゆる種類の異なる部分につながり、すべてが異なる副作用を引き起こすすべてのものに導く処理を行います。最終的にコードに到達するすべての場所を引き出しようとした場合、それは複雑なグラフに似ていて、潜在的にグラフのノードがあります。副作用を引き起こすと、それはあなたが単に機能が呼ばれているかだけでなく、その間にどの副作用が発生しているか、それらが発生している順序についても驚かされるかもしれません。

機能的言語は非常に複雑で再帰的な制御フローを持つことができますが、結果は、プロセスであらゆる種類の折lect的な副作用が起こっているわけではないため、正確性の点で非常に簡単に理解できます。複雑な制御フローが折lect的な副作用を満たしているときだけ、私はそれが何が起こっているのか、それが常に正しいことをするかどうかを理解しようとするために頭痛を引き起こすと思います。

そのため、これらのケースがある場合、そのようなコードの正確性について非常に自信を感じることは、不可能ではないにしても非常に難しいと感じます。したがって、私にとっての解決策は、制御フローを簡素化するか、副作用を最小化/統合することです(統合することにより、システム内の特定のフェーズでは、2つまたは3つ、またはAではなく、多くのことに1つのタイプの副作用を引き起こすようなものです。ダース)。私の2つのことのうちの1つが、存在するコードの正確性と私が導入する変更の正しさについて自信を感じるために、これら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番目のバージョンは、バグを引き起こすことなく変化するだけでなく、正確性の点で推論するのがはるかに簡単だと思います。とにかくそれが私がそれにアプローチする方法であり、私は同じ種類の考え方を適用して、イベントの取り扱いなどを簡素化するように、上記のメッシュ処理を簡素化します。

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

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

ライセンス: CC-BY-SA帰属
所属していません softwareengineering.stackexchange
scroll top