副作用は自然現象だと思います。しかし、それは関数型言語のタブーのようなものです。その理由は何ですか?
私の質問は、関数型プログラミングスタイルに固有のものです。すべてのプログラミング言語/パラダイムではありません。
副作用は自然現象だと思います。しかし、それは関数型言語のタブーのようなものです。その理由は何ですか?
私の質問は、関数型プログラミングスタイルに固有のものです。すべてのプログラミング言語/パラダイムではありません。
回答:
副作用のない関数/メソッドを記述することで、純粋な関数になります -プログラムの正確さについて推論するのが容易になります。
また、これらの関数を簡単に構成して新しい動作を作成できます。
また、コンパイラーが関数の結果をメモしたり、Common Subexpression Eliminationを使用したりできる特定の最適化も可能になります。
編集:Benjolの要求:状態の多くはスタックに保存されているため(Jonasがここで呼び出したように、制御フローではなくデータフロー)、独立した計算部分の実行を並列化または再順序付けできますお互い。1つのパーツが他のパーツに入力を提供しないため、これらの独立したパーツを簡単に見つけることができます。
スタックをロールバックしてコンピューティングを再開できるデバッガー(Smalltalkなど)の環境では、純粋な関数を使用することで、以前の状態を検査できるため、値の変化を非常に簡単に確認できます。突然変異が多い計算では、do / undoアクションを構造またはアルゴリズムに明示的に追加しない限り、計算の履歴を見ることができません。(これは最初の段落に戻ります:純粋な関数を書くと、プログラムの正確さを検査しやすくなります。)
関数型プログラミングに関する記事から:
実際には、アプリケーションにはいくつかの副作用が必要です。関数型プログラミング言語Haskellの主要な貢献者であるSimon Peyton-Jonesは、次のように述べています。箱が熱くなることを。」(http://oscon.blip.tv/file/324976)重要なのは、副作用を制限し、それらを明確に識別し、コード全体にそれらが散在しないようにすることです。
間違っていますが、関数型プログラミングは副作用の制限を促進し、プログラムの理解と最適化を容易にします。Haskellでさえ、ファイルに書き込むことができます。
基本的に、私が言っているのは、機能的なプログラマーは副作用を悪とは思わず、単に副作用の使用を制限することは良いと思うということです。私はそれがそのような単純な区別のように見えるかもしれないことを知っているが、それはすべての違いを生む。
readFile
は、アクションのシーケンスを定義しています。このシーケンスは機能的に純粋で、何をすべきかを説明する抽象的なツリーのようなものです。その後、実際のダーティな副作用がランタイムによって実行されます。
いくつかのメモ:
副作用のない関数は、ささいなことに並行して実行できますが、副作用のある関数は通常、何らかの同期を必要とします。
副作用のない関数は、より積極的な最適化を可能にします(結果キャッシュを透過的に使用するなど)。適切な結果が得られる限り、関数が実際に実行されたかどうかは関係ないためです
deterministic
副作用のない関数の句を提供するため、必要以上に頻繁に実行されることはありません。
deterministic
節は、これが決定論的な関数であることをコンパイラーに伝える単なるfinal
キーワードであり、Java のキーワードが変数を変更できないことをコンパイラーに伝える方法に匹敵します。
私は現在、主に機能的なコードで作業しており、その観点からは目がくらむほど明白です。副作用は、コードを読んで理解しようとするプログラマーに大きな精神的負担をかけます。あなたはしばらくの間それから解放されるまでその負担に気づかず、突然副作用のあるコードを再び読む必要があります。
この簡単な例を考えてみましょう。
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
を見たり、あまり理解したり、呼び出す関数の実装を見たりする必要さえありません。
並行性と並列化と最適化に関することはすべて素晴らしいことですが、それはコンピューター科学者がパンフレットに載せたものです。誰があなたの変数を変化させているのか不思議に思う必要はありません。いつ練習するのが本当に好きなのです。
副作用を引き起こすことを不可能にする言語はほとんどありません。完全に副作用がない言語は、非常に限られた能力を除いて、使用するのが非常に困難です(不可能に近い)。
なぜ副作用は悪とみなされるのですか?
それは、プログラムが何をするのかを正確に推論し、それがあなたが期待することをすることを証明するのをはるかに難しくするからです。
非常に高いレベルで、ブラックボックステストのみで3層のWebサイト全体をテストすることを想像してください。もちろん、規模に応じて実行可能です。しかし、確かに多くの重複が起こっています。あればあるバグが(それが副作用に関連している)バグが診断され、固定され、修正がテスト環境にデプロイされるまで、あなたは潜在的に、更なる試験のために、システム全体を破ることができました。
利点
今、それを縮小します。副作用のないコードを書くのがかなり上手だった場合、既存のコードが何をしたかを推論するのにどれくらい速くなりますか?ユニットテストをどれくらい速く書くことができますか?副作用のないコードにはバグがないことが保証されていること、そしてユーザーがそれが持っているバグへの露出を制限できるとどの程度自信がありますか?
コードに副作用がない場合、コンパイラは実行可能な追加の最適化を行う場合があります。これらの最適化を実装する方がはるかに簡単かもしれません。副作用のないコードの最適化を概念化する方がはるかに簡単かもしれません。つまり、コンパイラベンダーは、副作用のあるコードでは不可能な最適化を実装する可能性があります。
また、並行性は、コードに副作用がない場合の実装、自動生成、および最適化が大幅に簡素化されます。これは、すべてのピースを任意の順序で安全に評価できるためです。プログラマーが高度な並行コードを記述できるようにすることは、コンピューターサイエンスが取り組む必要がある次の大きな課題であり、ムーアの法則に対する数少ない残りのヘッジの1つです。
副作用は、コード内の「リーク」のようなものであり、後から自分または疑いを持たない同僚が処理する必要があります。
関数型言語は、コードをコンテキストに依存せず、よりモジュール化する方法として、状態変数と可変データを回避します。モジュール性により、ある開発者の作業が別の開発者の作業に影響を与えたり、損なわれたりすることがなくなります。
チームの規模に応じた開発率のスケーリングは、今日のソフトウェア開発の「聖杯」です。他のプログラマと作業するとき、モジュール性ほど重要なことはほとんどありません。最も単純な論理的な副作用であっても、コラボレーションは非常に困難です。
まあ、私見、これは非常に偽善的です。副作用が好きな人はいませんが、誰もがそれを必要としています。
副作用について非常に危険なのは、関数を呼び出すと、関数が次に呼び出されたときの動作だけでなく、他の関数にも影響する可能性があることです。したがって、副作用により、予測できない動作や重要な依存関係が発生します。
オブジェクト指向や関数型などのプログラミングパラダイムは両方ともこの問題に対処します。OOは、関心の分離を課すことで問題を軽減します。これは、多くの可変データで構成されるアプリケーションの状態がオブジェクトにカプセル化されることを意味します。各オブジェクトは、独自の状態のみを維持する責任があります。これにより、依存関係のリスクが軽減され、問題がはるかに分離され、追跡しやすくなります。
関数型プログラミングは、プログラマーの観点からはアプリケーションの状態が単純に不変である、はるかに過激なアプローチを取ります。これはいい考えですが、言語自体を役に立たないものにします。どうして?すべてのI / O操作には副作用があるためです。入力ストリームから読み取るとすぐに、アプリケーションの状態が変わる可能性があります。同じ関数を次に呼び出すと、結果が異なる可能性が高いためです。異なるデータを読み込んでいるか、または可能性として-操作が失敗する可能性があります。出力についても同様です。偶数出力は副作用のある操作です。これは最近よく気づいていることではありませんが、出力に20Kしかないことを想像してください。それ以上出力すると、ディスク容量が足りないなどの理由でアプリがクラッシュします。
そのため、プログラマの観点からすると、副作用は厄介で危険です。ほとんどのバグは、アプリケーションの状態の特定の部分が、ほとんど考慮されていない、多くの場合、不必要な副作用によってインターロックされる方法に由来します。ユーザーの観点から見ると、副作用はコンピューターを使用するポイントです。彼らは内部で何が起こるか、それがどのように組織化されるかを気にしません。彼らは何かをし、それに応じてコンピューターが変化することを期待しています。
副作用があると、テスト時に考慮する必要がある追加の入力/出力パラメーターが導入されます。
これにより、検証対象のコードだけに環境を制限することはできず、周囲の環境の一部またはすべてを取り込む必要があるため、コード検証がはるかに複雑になります(更新されるグローバルはそのコード内に存在し、それは次に依存しますコードは、完全なJava EEサーバー内での生活に依存します。...)
副作用を回避しようとすると、コードの実行に必要な外部性の量を制限できます。
私の経験では、オブジェクト指向プログラミングの優れた設計では、副作用のある関数の使用が義務付けられています。
たとえば、基本的なUIデスクトップアプリケーションを取り上げます。私のプログラムのドメインモデルの現在の状態を表すオブジェクトグラフがヒープ上にある実行中のプログラムがあります。メッセージはそのグラフのオブジェクトに到着します(たとえば、UIレイヤーコントローラーから呼び出されたメソッド呼び出しを介して)。ヒープ上のオブジェクトグラフ(ドメインモデル)は、メッセージに応じて変更されます。モデルのオブザーバーには変更が通知され、UIやその他のリソースが変更されます。
邪悪ではなく、これらのヒープ変更およびスクリーン変更の副作用の正しい配置は、OOデザイン(この場合はMVCパターン)の中核にあります。
もちろん、それはあなたのメソッドが任意の副作用を持つべきであることを意味しません。また、副作用のない関数は、コードの可読性を向上させ、場合によってはパフォーマンスを向上させることができます。
上記の質問が指摘したように、関数型言語は、特定のコードでいつ副作用が発生するかを管理するためのツールを提供するので、コードが副作用を防ぐことはあまりありません。
これは非常に興味深い結果をもたらすことが判明しました。第一に、そして明らかに、副作用のないコードでできることは数多くありますが、それらはすでに説明されています。ただし、副作用のあるコードを使用する場合でも、他にもできることがあります。
複雑なコードベースでは、副作用の複雑な相互作用は、私が推論する最も難しいものです。脳の働き方を考えれば、個人的にしか話せません。副作用や永続的な状態、入力の変化などにより、個々の機能で「何が」起こっているかだけでなく、「いつ」「どこで」正しいことを理由に考える必要があります。
「何」だけに集中することはできません。呼び出し元が間違ったタイミングで、間違ったスレッドから、間違った方法で呼び出すことでそれを誤用する可能性があるため、副作用を引き起こす関数を徹底的にテストした後、それを使用するコード全体に信頼性の空気を広めるという結論を出すことはできません注文。一方、副作用を引き起こさず、入力が与えられると(入力に触れることなく)新しい出力を返す関数は、この方法で誤用することはほとんど不可能です。
しかし、私は実用的なタイプだと思う、または少なくともそうしようとしています。そして、コードの正確さを推論するために、すべての副作用を最小限に抑える必要はないと思います(少なくとも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番目のバージョンのほうがはるかに簡単に推論できることです。そのため'
結局のところ、ある時点で副作用が発生する必要があります。そうでなければ、どこにも行かないデータを出力する関数が必要になります。多くの場合、ファイルに何かを記録し、画面に何かを表示し、この種のソケットを介してデータを送信する必要がありますが、これらはすべて副作用です。しかし、余分な副作用の数を確実に減らすことができ、制御フローが非常に複雑な場合に発生する副作用の数も減らすことができます。
それは悪ではありません。私の意見では、副作用がある場合とない場合の2つの関数タイプを区別する必要があります。副作用のない関数:-同じ引数で常に同じを返すため、たとえば引数のないこのような関数は意味がありません。-それはまた、そのようないくつかの関数が呼び出される順序は何の役割も果たさないことを意味します-他のコードなしで、単独で(!)実行でき、デバッグできる必要があります。そして今、笑、JUnitが作るものを見てください。副作用のある関数:-一種の「リーク」、自動的に強調表示できるもの-デバッグと間違いの検索により、一般的に副作用によって引き起こされるものが非常に重要です。-副作用のある機能には、副作用のない「部分」もあり、自動的に分離することもできます。これらの副作用は悪です