純粋関数:「副作用なし」は「同じ入力が与えられた場合、常に同じ出力」を意味しますか?


84

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

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

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

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


3
あなたの施設は明確に指定されていません。「入力」が広すぎます。関数は2種類の入力があると考えることができます。彼らの議論、そして「環境的」/「文脈的」。これらの2種類の入力を区別しない場合、システム時刻を返す関数は純粋であると見なすことができます(それは明らかではありませんが)。
アレクサンダー-モニカを復活させる

4
@Alexander:「純粋関数」のコンテキストでは、「入力」は一般に、(プログラミング言語が使用するメカニズムによって)明示的に渡されるパラメーター/引数を意味すると理解されています。それは「純粋関数」の定義の一部です。しかし、あなたは正しいです、それは定義を気にすることが重要です。
sleske

3
些細な反例:グローバル変数の値を返します。副作用はありませんが(グローバルはこれまでに読み取られただけです!)、それでも毎回異なる結果になる可能性があります。(グローバルが気に入らない場合は、実行時の呼び出しスタックに依存するローカル変数のアドレスを返します)。
ピーター-モニカを復活させる

2
「副作用」の定義を拡張する必要があります。純粋な方法では副作用が発生しないと言いますが、純粋な方法では他の場所で発生する副作用が発生しないことにも注意する必要があります。
EricLippert19年

2
@sleskeおそらく一般的に理解されていますが、その区別の欠如がOPの混乱の正確な原因です。
アレクサンダー-モニカを復活させる

回答:


114

外側のスコープを変更しないが、それでも不純であると見なされるいくつかの反例を次に示します。

  • 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つの条件を補完的なものと考えています。

  • 結果の評価が側面の状態に影響を与えなりません
  • 評価結果は側面状態の影響を受けてはなりません

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


1
ベルギに感謝します。どういうわけか、副作用にはローカルスコープ外の変数の読み取りが含まれていると思いましたが、そのような外部変数を書き込んだ場合の副作用にすぎないと思います。
マグナス

17
prompt("you choose")副作用がない場合は、一歩下がって副作用の意味を明確にする必要があります。
ホルガー

1
@Magnusはい、まさにそれが効果の意味です。私も私の答えで明確にしようとします、私はそれほど大きな注目を期待していなかったので、数十票に値する答えを作りたいです:-)
ベルギ

2
ご存知のとおり、Math.random()はサーマルダイオードを返します。不良RNGを使用するように実際に指定されているわけではありません。
ジョシュア

1
2つの条件のうち、前者は「効果」と呼ばれ、後者は「共効果」と呼ばれると聞きました。どちらも「副作用」であり、不純です。f(共効果、入力)->効果、出力共効果は、より広い環境の変化から生じる入力であり、効果は、より広い環境を変化させる出力です。たとえば、ElmとClojurescripsのリフレームは、このモデルで機能します。

30

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

参照透過性とは、大まかに言って、プログラムの意味を変更せずに、プログラムの任意の時点で関数の呼び出しをその戻り値に、またはその逆に置き換えることができることを意味します。

したがって、たとえば、Cprintfが参照透過性である場合、これら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関数は、no-opでない限り、参照透過性になることはありません。

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

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配列を使用しているため、参照透過性がなく、純粋ではないと簡単に結論付けることができます。ただし、関数を見ると、参照透過性であり、したがって純粋であることわかります。これは、外部純度と呼ばれることもあります。つまり、外の世界には純粋に見えるが、内部では不純に実装されている機能です。

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


2
並行性が得られると、その不純物はプログラム全体に影響します。
R .. GitHub STOP

@R ..並行性により、説明されているフィボナッチ関数が外部的に不純になる可能性がある方法を考えられますか?できません。書き込みはべきmemo[n]等であり、読み取りに失敗するとCPUサイクルが無駄になるだけです。
ブリリアン

私はあなたの両方に同意します。不純物並行性の問題引き起こす可能性がありますが、この特定のケースではそうではありません。
イェルクWミッターク

@R ..並行性を意識したバージョンを想像するのは難しいことではありません。
user2537 5119年

1
@Brilliandたとえば、memo[n] = ...最初に辞書エントリを作成してから、そのエントリに値を格納する場合があります。これにより、別のスレッドが初期化されていないエントリを確認できるウィンドウが残ります。
user2537 5119年

12

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

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

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

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

純粋関数とは、厳密に言えば、次の特性を満たす関数です。 参照透過性の。これは、プログラムの動作を変更せずに、関数適用を生成する値に置き換えることができるプロパティです。

以下を追加するだけの関数があるとします。

function addOne(x) {
  return x + 1;
}

私たちは、置き換えることができaddOne(5)6、私たちのプログラムではどこでも、何も変更されます。

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

addOneAndLog(x)出力を返す以外に実行されるこの余分な動作は、副作用と見なされます。


「あなたが説明した2番目の条件は最初の条件よりも弱い制約であるように私には思えます。」いいえ、2つの条件は論理的に独立しています。
sleske

@sleskeあなたは間違っています。純粋および副作用という用語の明確な定義を提供しました。これらの制約の範囲内で、副作用のない関数が特定の入力に対して同じ出力を返すことはありません。ただし、最初の条件がなくても2番目の条件が満たされる例を示しました。純度の概念を理解するための基本的な概念は、参照透過性です。
TheInnerLight

小さなタイプミス:特定の入力に対して同じ出力を返す以外に、副作用のない関数でできることは何もありません。
TheInnerLight

現在の時刻を返すようなものはどうですか?副作用はありませんが、同じ入力に対して異なる出力を返します。または、より一般的には、結果が入力パラメーターだけでなく、(変更可能な)グローバル変数にも依存する関数。
sleske

2
一般的に使用されているものとは異なる「副作用」の定義を使用しているようです。副作用は通常、「値を返す以外の観察可能な効果」または「状態の観察可能な変化」として定義されます。たとえば、Wikipediasoftwareengineering.SEに関するこの投稿を参照してください。あなたは完全に正しいです。それDate.now()は純粋/参照透過性ではありませんが、副作用があるからではなく、その結果が入力だけではないからです。
sleske

7

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

とにかく、私が考えることができるすべて。


3
私によると、これらの「システム外部からのランダム性」は副作用の一形態です。これらの動作をする関数は「純粋」ではありません。
ジョセフM.ディオン

2

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

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

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

しかし、すべての数学関数も純粋であるとはt限りません。パラメーターとして(時間)を持つ各関数は不純である可能t性があります。関数のすべての効果と確率的性質を保持します。通常、入力信号があり、実際の値がわからない場合は、次のことができます。ノイズさえあります。


2

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

はい

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

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

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

一緒に組み合わせたときのポイント#1と#2の両方の全体的な効果は、次のことを意味します。同じi / pを持つ関数Sumがプログラムの結果に置き換えられた場合、プログラムの全体的な意味は変わりません。これは参照透過性に他なりません。


ただし、この場合、最初の条件は検証されません。コンソールへの書き込みは、マシン自体の状態を変更するため、副作用と見なされます。
右脚

それを指摘してくれた@Rightlegthx。どういうわけか私はOPをまったく別の方法で誤解しました。正解。
rahulaga_dev

2
ランダムジェネレーターの状態を変えませんか?
EricDuminil19年

1
乱数生成器の状態は、関数が条件2を満足するであろう明示的に供給されていない限り、乱数を生成すること、副作用自体である
TheInnerLight

1
rndは関数をエスケープしないため、状態が変化するという事実は関数の純度には関係ありませんが、Randomコンストラクターが現在の時刻をシード値として使用するという事実は、aと以外の「入力」があることを意味しbます。
Sneftel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.