プログラミング言語や副作用のないプログラムについて理由を説明する方が簡単なのはなぜですか?


8

リチャード・P・ガブリエルから「Yの理由」を読みました。Yコンビネーターに関する読みやすい記事ですが、めったにありません。記事は、階乗関数の再帰的な定義から始まります。

(letrec ((f (lambda (n)
              (if (< n 2) 1 (* n (f (- n 1)))))))
  (f 10))

そしてそれはletrec副作用で定義できることを説明します:

(let ((f #f))
  (set! f (lambda (n)
            (if (< n 2) 1 (* n (f (- n 1))))))
  (f 10))

また、記事の残りの部分ではletrec、Yコンビネーターを使用して定義することもできると説明しています。

(define (Y f)
  (let ((g (lambda (h)
             (lambda (x)
               ((f (h h)) x)))))
    (g g)))

(let ((f (Y (lambda (fact)
              (lambda (n)
                (if (< n 2) 1 (* n (fact (- n 1)))))))))
  (f 10))

明らかに、これは副作用のあるバージョンよりもはるかに複雑です。副作用よりもYコンビネーターを優先することが有益である理由は、次のステートメントによってのみ示されます。

副作用のないプログラミング言語やプログラムについて考えるのは簡単です。

これについてはこれ以上説明しません。説明を探してみます。


その「考えやすい」行は純粋な宣伝です。それは常に信仰記事として与えられます-証拠は必要とされず、提供されません-批判的に分析されたとき、それは笑いテストにさえ合格しません。お気づきのように、Y Combinatorのバージョンが2倍以上複雑であることは自明です。
メイソンウィーラー、

5
@MasonWheeler可変オブジェクトをいくつかのメソッドに渡すと、それが純粋に入力として使用されている場所と、その場所で変更されている場所を区別することが難しくなります。機能的な代替手段-オブジェクトの新しいコピーを返す-はそれを明確にします。純粋が常に優れているとは言いませんが、可変オブジェクトの大きなグラフが理由を簡単に説明できると主張するのは困難です。目に見えないコンテキストが多すぎます。
Doval

@Dovalオブジェクトの複数のコピーが実行されている場合、どのように「明確化」されますか。一部は廃止され、一部は正規のものであり、今はそれをまっすぐに保つ必要がありますか?それはさらに混乱しそうですね!(または、代わりに、手動のメモリ管理とまったく同じタスクであるセカンダリコピーへの参照ないことを確認する必要があります。FPは、必要を回避するためにガベージコレクションを発明したことについて、sooooooが混乱し、推論するのが難しいと判断しました。行うには!)
メイソンウィーラー、

2
@MasonWheelerデータが変更されることになっている場合でも、誰がデータを変更しているかを制御したいとします。データを変更することを想定していないメソッドに渡したいのですが、誰かが失敗して、とにかくデータを変更してしまうバグを引き起こす可能性があります。次に、「防御的なコピー」を作成し(実際には、Effective Javaブックの推奨事項です)、最初から不変のデータ構造を使用するよりも多くの作業/ガベージの生成を行います。データが変更されるという事実は、不変の文字列または数値型を使用する人の邪魔になることはありません。
Doval

2
@MasonWheeler FP言語は大量のガベージを生成しません。そうしないと、役に立たなくなります。それは彼らが「舞台裏」で働く方法ではありません。「推論しやすい」とは、通常、等式推論を指しますが、これは笑い事ではありません。等式推論は多くのパラダイムで行うことができますが、成功の度合いは異なりますが、FP言語では通常はより簡単で、それは大きな勝利です(ただし、他のものを犠牲にして、すべてが人生のトレードオフです)。
アンドレスF.17年

回答:


13

明らかに、はるかに読みやすい副作用のある関数と同じ計算を実行する、非常に読みにくい純粋な関数の例を見つけることができます。特に、Yコンビネーターのような機械的変換を使用して解に到達する場合。それは、「推論しやすい」という意味ではありません。

副作用のない関数について推論する方が簡単な理由は、入力と出力だけを考慮すればよいからです。副作用があるため、関数が呼び出される回数、関数が呼び出される順序、関数内で作成されるデータ、共有されるデータ、コピーされるデータについても考慮する必要があります。 そして、あなたが呼んでいる関数の内部で、そしてそれらの関数の内部で再帰的に呼び出されるかもしれないすべての関数のためのすべてのその情報など。

この効果は、おもちゃのサンプル関数よりも、複数のレイヤーを含む製品コードではるかに簡単に確認できます。ほとんどの場合、関数の型シグネチャだけに頼ることができます。純粋な関数型プログラミングをしばらく行ってから戻ってくると、副作用の負担に本当に気づきます。


10

副作用のない言語の興味深い特性は、並列処理、並行処理、または非同期を導入してもプログラムの意味を変更できないことです。それはそれをより速くすることができます。またはそれはそれを遅くすることができます。しかし、それはそれを誤解することはできません。

これにより、プログラムを自動的に並列化するのは簡単です。実際、些細なことですが、通常は、並列処理が多すぎます。GHCチームは自動並列化を実験しました。彼らは、単純なプログラムでも数百、さらには数千のスレッドに分解できることを発見しました。これらすべてのスレッドのオーバーヘッドは、潜在的なスピードアップを数桁も圧倒します。

したがって、機能的プログラムの自動並列化の場合、問題は「小さなアトミック操作をどのようにして有用なサイズの並列ピースにグループ化するのか」になり、不純なプログラムでは「大規模なモノリシック操作をどのように有用なものに分解するのか」という問題になります。平行ピースのサイズ」。これの良い点は、前者はヒューリスティックに実行できることです(覚えておいてください:間違った場合は、プログラムの実行速度が少し遅くなる可能性があります)。後者はホールティングを解決することと同じです。問題(一般的なケース)、そしてそれを間違えた場合、プログラムは単純にクラッシュする(運が良ければ!)か、微妙に間違った結果(最悪の場合)を返します。


それはまた、デッドロックやライブロック、どちらかが任意のより良いではないことを...でした
デュプリケータ

6

副作用のある言語では、エイリアス解析を使用して、関数の呼び出し後にメモリ位置を再読み込みする必要があるかどうかを確認します。この分析の保守性は言語によって異なります。

Cの場合、言語はタイプセーフではないため、これはかなり保守的でなければなりません。

JavaとC#の場合、型の安全性が向上しているため、これらはそれほど保守的である必要はありません。

過度に保守的であると、負荷の最適化が妨げられます。

このような分析は、副作用のない言語では不要です(または、見方によっては簡単です)。


エイリアスは可変変数と参照の両方でのみ可能であることに注意してください。どちらか一方のみの言語にはこの問題はありません
gardenhead 2017年

4

あなたが与えるどんな仮定でも利用するための最適化は常にあります。操作の並べ替えが思い浮かびます。

頭に浮かぶ1つの面白い例は、一部の古いアセンブリ言語に実際に現れます。特にMIPSには、どの分岐が行われたかに関係なく、条件付きジャンプ後の命令が実行されるというルールがありました。これを望まない場合は、ジャンプの後にNOPを置きます。これは、MIPSパイプラインの構成方法が原因で行われました。条件付きジャンプの実行に組み込まれた自然な1サイクルのストールがあったので、そのサイクルで何か便利なことをすることもできます!

コンパイラーは多くの場合、両方のブランチで実行する必要がある操作を探し、そのスロットにスライドして、もう少しパフォーマンスを確認します。ただし、コンパイラーがそれを実行できないが、操作に副作用がないことを示すことができる場合、コンパイラー日和見的にそれをその場所に固定することができます。したがって、1つのパスでは、コードは1つの命令をより速く実行します。もう一方のパスでは、害はありません。


1

「letrecは副作用で定義できます...」定義に副作用はありません。はい、set!Schemeで副作用を生成する一般的な方法であるwhichを使用しますが、この場合、副作用はありません - fは完全にローカルであるため、ローカル以外の関数から参照することはできません。したがって、外部スコープから見た場合の副作用ではありません。このコード、Schemeのスコープの制限をハックして、デフォルトではラムダ定義がそれ自体を参照することを許可していません。

他のいくつかの言語には、定数の値を生成するために使用される式が定数自体を参照できる宣言があります。そのような言語では、正確に同等の定義を使用できますが、定数のみが使用されるため、これは明らかに副作用を生じません。たとえば、次の同等のHaskellプログラムを参照してください。

let f = \ n -> if n < 2 
                 then 1 
                 else n*(f (n-1)) 
        in (f 5)

(120と評価されます)。

これには明らかに副作用はありません(Haskellの関数に副作用があるため、モナドでラップされた結果を返す必要がありますが、ここで返される型は単純な数値型です)。あなたが引用するコード。


letローカル関数を返す可能性があるため、一般的には副作用です。
2016

2
@ceving-それでも、副次的な影響はありません。格納場所の変更は、他のコードがそれを読み取ることができる前の時点に制限されているためです。副作用が現実になるためには、外部エージェントがそれが起こったことに気付くことが可能でなければなりません。この場合、それが発生する可能性のある方法はありません。
Periata Breatta

0

これについてはこれ以上説明しません。説明を探してみます。

これは、大規模なコードベースをデバッグしてきた私たちの多くに固有のことですが、それを理解するには、十分長い時間、監督者レベルで十分な規模に対処する必要があります。それはポーカーでのポジションの重要性を理解するようなものです。最初は、100万ハンドのハンド履歴を記録し、アウトよりもはるかに多くのお金を獲得したことに気づくまで、すべてのターンの最後に最後に行くのは、それほど有益な利点ではないようです。

とはいえ、ローカル変数への変更は副作用であるという考えには同意しません。私の見解では、関数はスコープ外で何も変更しなければ副作用を引き起こしません。つまり、関数が触れたり改ざんしたりしても、コールスタックの下にあるものや、関数自体が取得しなかったメモリやリソースには影響しません。 。

一般に、想像できる最も完璧なテスト手順を持たない複雑で大規模なコードベースで最も難しいのは、永続的な状態管理です。たとえば、ビデオゲームの世界での細かいオブジェクトへのすべての変更は、何千もの関数が、実際にシステム全体の不変式に違反した容疑者の無限のリストの中から絞り込もうとしている(「これは決して起こらないはずです、だれがやったのですか?」)。関数の外で何も変更されていない場合、集中的な誤動作を引き起こす可能性はありません。

もちろん、これはすべての場合に実行できるわけではありません。別のマシンに保存されているデータベースを更新するアプリケーションは、本来、副作用を引き起こすように設計されています。また、ファイルをロードして書き込むように設計されているアプリケーションも含まれています。しかし、たとえば、不変のデータ構造の豊富なライブラリがあり、この考え方をさらに取り入れれば、多くの関数や多くのプログラムで副作用なしに実行できることはまだまだたくさんあります。

おかしなことに、John Carmackは、最も微調整されたCコーディングの時代から始まったにもかかわらず、LISPと不変性の可能性に飛びついています。Cを今でもよく使っていますが、私も同じようなことをしています。これは、何年にもわたってデバッグを行い、複雑で大規模なシステム全体を監督者レベルから推論しようと努めてきた実用主義者の性質だと思います。関数呼び出しの最も複雑な相互接続されたグラフ間で変更されている複雑な永続状態がたくさんある場合、驚くほど堅牢でバグが大幅に少ないものでも、何か問題が角を曲がっていることに不安を感じることがあります。数百万行のコード。すべての単一のインターフェースが単体テストでテストされ、すべて合格したとしても、

実際には、関数型プログラミングでは関数の理解が難しくなることがよくあります。それでも、特に複雑な再帰的なロジックでは、脳がねじれたり結び目になったりします。しかし、関数型言語で記述されたいくつかの関数を理解することに関連するすべての知的オーバーヘッドは、永続的な状態が数万の関数にわたって変化する複雑なシステムのそれよりも小さく、副作用を引き起こす各関数の合計が合計になります。全体としてのシステム全体の正確さについての推論の複雑さ。

関数に副作用を回避させるために、純粋な関数型言語は必要ありません。関数で変更されたローカル状態は、副作用forとしてカウントされません。たとえば、関数にローカルなループカウンター変数は副作用としてカウントされません。私は今日、可能な限り副作用を回避することを目的としてCコードを作成し、残りのデータが浅いコピーされている間に部分的に変更できる不変でスレッドセーフなデータ構造のライブラリを自分で考案しました。私のシステムの正しさについてはかなりの理由があります。その意味で私は著者に強く反対します。少なくともミッションクリティカルなソフトウェアのCおよびC ++では、関数は、関数の外部の世界に影響を与える可能性のあるものに触れない場合、副作用がないと文書化できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.