web-dev-qa-db-ja.com

純粋な機能:「副作用なし」は「常に同じ出力を与えられ、同じ出力を与える」ことを意味しますか?

関数をpureとして定義する2つの条件は次のとおりです。

  1. 副作用なし(つまり、ローカルスコープへの変更のみが許可されます)
  2. 同じ入力の場合、常に同じ出力を返します

最初の条件が常にtrueである場合、2番目の条件がtrueにならないことがありますか?

つまりそれは本当に最初の条件でのみ必要ですか?

83
Magnus

以下は、外側のスコープを変更しないが不純と見なされるいくつかの反例です。

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); }(確かにPRNGを変更しますが、監視可能とは見なされません)

2番目の条件に違反するには、非定数の非ローカル変数にアクセスするだけで十分です。

私は常に、純度の2つの条件を補完的なものとして考えています。

  • 結果の評価はサイドステートに影響を与えてはなりません
  • 評価結果は、サイドステートの影響を受けない

副作用 という用語は、最初の、非ローカル状態を変更する関数のみを指します。ただし、場合によっては読み取り操作も副作用と見なされます。それらが操作であり、主な目的が値へのアクセスであっても、書き込みを伴う場合。その例として、ジェネレータの内部状態を変更する疑似乱数の生成、読み取り位置を進める入力ストリームからの読み取り、または「測定の実行」コマンドを含む外部センサーからの読み取りがあります。

113
Bergi

純粋な関数が何であるかを表現する「通常の」方法は、参照透明度の観点からです。関数は純粋である場合参照的に透明です。

Referential Transparencyは、おおよそ、プログラムの意味を変更せずに、プログラムの任意の時点で関数の呼び出しを戻り値に、またはその逆に置き換えることができることを意味します。

したがって、たとえば、Cのprintfが参照透過的である場合、これら2つのプログラムは同じ意味を持つはずです。

printf("Hello");

そして

5;

次のプログラムはすべて同じ意味を持つ必要があります。

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

printfは書き込まれた文字数を返すため、この場合は5です。

void関数を使用すると、さらにわかりやすくなります。関数がある場合void foo、次に

foo(bar, baz, quux);

と同じである必要があります

;

つまりfooは何も返さないので、プログラムの意味を変更せずに何も置き換えられないはずです。

したがって、printffooも参照として透過的ではなく、したがってどちらも純粋ではないことは明らかです。実際、void関数は、何もしない場合を除いて、参照を透過的にすることはできません。

私はあなたが与えたものと同じようにこの定義を扱う方がはるかに簡単だと思います。また、任意の粒度で適用することもできます。個々の式、関数、プログラム全体に適用できます。たとえば、次のような関数について話すことができます。

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

関数を構成する式を分析して、それらが参照可能な透過性を持たず、したがって純粋でないと簡単に結論付けることができます。これは、それらが可変データ構造、つまりmemo配列を使用しているためです。ただし、関数を見ると、それがis参照的に透過的であり、純粋であることもわかります。これはexternal Purityと呼ばれることもあります。つまり、外の世界からは純粋に見えますが、内部では不純に実装されている関数です。

そのような関数は依然として有用です。不純物はその周囲のすべてに感染しますが、外部の純粋なインターフェースは一種の「純度の障壁」を構築するため、不純物は関数の3行にのみ感染し、プログラムの他の部分には漏れません。 。これらの3行は、プログラム全体よりも正確性の分析がはるかに簡単です。

29
Jörg W Mittag

あなたが説明した2番目の条件は、最初の条件よりも弱い制約であるように思えます。

例を挙げましょう。コンソールにもログを記録する関数を追加するとします。

_function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}
_

指定した2番目の条件が満たされます。この関数は、同じ入力が与えられると常に同じ出力を返します。ただし、コンソールへのロギングの副作用が含まれているため、純粋な関数ではありません。

純粋な関数は、厳密に言えば、 参照透明度 のプロパティを満たす関数です。これは、プログラムの動作を変更せずに、関数アプリケーションを生成する値で置き換えることができるプロパティです。

単純に追加する関数があるとします。

_function addOne(x) {
  return x + 1;
}
_

プログラムの任意の場所でaddOne(5)を_6_に置き換えることができ、何も変更されません。

対照的に、最初の式ではコンソールに何かが書き込まれるのに対して2番目の式では行われないため、動作を変更せずにaddOneAndLog(x)を値_6_で置き換えることはできません。

出力を返す以外に、addOneAndLog(x)が実行するこの追加の動作を副作用と見なします。

12
TheInnerLight

システムの外部からランダム性のソースが存在する可能性があります。計算の一部に室温が含まれているとします。次に、関数を実行すると、室温のランダムな外部要素に応じて、毎回異なる結果が得られます。プログラムを実行しても状態は変化しません。

とにかく私が思いつくすべて。

7
user3340459

FP定義の問題は、それらが非常に人為的であることです。各評価/計算には、評価者に副作用があります。それは理論的には真です。これの否定は、FP謝罪者は哲学と論理を無視します:「評価」とは、あるインテリジェント環境(機械、脳など)の状態の変化を意味します。これは評価プロセスの性質です。変更なし-「計算」なし非常に目立つ:CPUまたはその障害の加熱、過熱した場合のマザーボードのシャットダウンなど。

参照透過性について話すとき、そのような透過性に関する情報は、システム全体の作成者および意味情報の保持者として人間が利用でき、コンパイラーが利用できない場合があることを理解する必要があります。たとえば、関数は一部の外部リソースを読み取ることができ、シグネチャにIOモナドが含まれますが、常に同じ値が返されます(たとえば、current_year > 0 )コンパイラは、関数が常に同じ結果を返すことを認識していないため、関数は不純ですが、参照透過プロパティを持ち、True定数で置き換えることができます。

したがって、このような不正確さを回避するには、数学関数とプログラミング言語の「関数」を区別する必要があります。 Haskellの関数は常に不純であり、それらに関連する純度の定義は常に非常に条件付きです。それらは、実際の副作用と物理的特性を備えた実際のハードウェアで実行されていますが、これは数学関数では間違っています。これは、「printf」関数の例が完全に正しくないことを意味します。

しかし、すべての数学関数も純粋であるとは限りません。パラメータとしてt(時間)を持つ各関数は不純かもしれません:tは、関数のすべての効果と確率論的性質を保持します。入力信号と実際の値についての考えを持っていない場合、それはノイズでさえありえます。

2
Paul-AG

最初の条件が常にtrueである場合、2番目の条件がtrueにならないことがありますか?

はい

以下の簡単なコードスニペットを検討してください

public int Sum(int a, int b) {
    Random rnd = new Random();
    return rnd.Next(1, 10);
}

このコードは、指定された同じ入力セットに対してランダムな出力を返しますが、副作用はありません。

一緒に組み合わせたときに言及したポイント#1と#2の両方の全体的な効果は、同じi/pの関数Sumがプログラムの結果、プログラムの全体的な意味は変わりません。これは 参照透明度 に他なりません。

2
rahulaga_dev