参照の透明性を壊す副作用


11

Scalaの関数型プログラミングは、参照の透明性を壊すことに対する副作用の影響を説明しています。

副次的影響。これは、参照の透明性に対する何らかの違反を意味します。

「置換モデル」を使用してプログラムを評価するSICPの一部を読みました。

参照透過性(RT)を使用した置換モデルを大まかに理解しているので、関数を最も単純な部分に分解できます。式がRTの場合、式を分解して常に同じ結果を得ることができます。

ただし、上記の引用が述べているように、副作用を使用すると置換モデルが壊れる可能性があります。

例:

val x = foo(50) + bar(10)

副作用がない場合はfoo、どちらの関数を実行しても常に同じ結果がに返されます。しかし、それらに副作用がある場合、それらはレンチを置換モデルに混乱させる/投げる変数を変更します。bar x

私はこの説明に満足していますが、完全に理解していません。

私を修正して、RTを壊す副作用についての穴を埋めて、置換モデルへの影響についても議論してください。

回答:


20

参照透過性の定義から始めましょう。

プログラムの動作を変更せずに式をその値に置き換えることができる(つまり、同じ入力で同じ効果と出力を持つプログラムを生成する)場合、その式は参照上透過的であるといいます。

つまり、(たとえば)プログラムのどの部分でも2 + 5を7に置き換えることができ、プログラムは引き続き機能します。このプロセスは置換と呼ばれます。 置換は、プログラムの他の部分に影響を与えずに 2 + 5を7に置き換えることができる場合にのみ有効です。

Baz関数FooとそのBar中にと呼ばれるクラスがあるとしましょう。簡単にするために、私たちはそれを言うだけでFooBarどちらも渡された値を返しFoo(2) + Bar(5) == 7ます。参照の透明性により、プログラム内の任意の場所で式Foo(2) + Bar(5)を式に置き換えることができ7、プログラムは引き続き同じように機能します。

しかしFoo、渡された値がBar返されたが、渡された値と、最後に提供されFooた値が返された場合はどうなりますか?クラスFoo内のローカル変数にの 値を格納する場合、これは十分簡単ですBaz。まあ、そのローカル変数の初期値が0の場合、式Foo(2) + Bar(5)7最初に呼び出したときに期待される値を返し9ますが、2回目に呼び出したときには返します。

これは、参照の透明性に2つの方法で違反します。まず、Barが呼び出されるたびに同じ式を返すことは期待できません。次に、副作用が発生しました。つまり、Fooを呼び出すと、Barの戻り値が影響を受けます。Foo(2) + Bar(5)が7になることを保証できないため、代替することはできません。

これが実際に参照の透明性が意味することです。参照透過関数は、プログラムの他の場所にある他のコードに影響を与えることなく、いくつかの値を受け入れ、いくつかの対応する値を返し、同じ入力が与えられると常に同じ出力を返します。


5
破壊だから、RTあなたが使用してから無効にsubstitution model.して大きな問題ではない使用することができることは、substitution modelプログラムについての理由にそれを使用しての力ですか?
Kevin Meredith

その通りです。
Robert Harvey

1
素晴らしく明確で理解できる答えを+1します。ありがとうございました。
ラチェット2014年

2
また、これらの関数が透過的または「純粋」である場合、実際に実行される順序は重要ではありません。foo()またはbar()が最初に実行されるかどうかは気にせず、場合によっては、不要な場合に評価されないことがあります
Zachary K

1
RTのさらにもう1つの利点は、高価な参照透過式をキャッシュできることです(1回または2回評価するとまったく同じ結果が得られるため)。
dcastro

3

あなたが壁を構築しようとしていて、さまざまなサイズと形のさまざまな箱が与えられたとします。壁の特定のL字型の穴を埋める必要があります。L字型の箱を探すべきですか、それとも、適切なサイズの2つのまっすぐな箱で代用できますか?

機能的な世界では、どちらのソリューションでも機能するというのが答えです。機能的な世界を構築するとき、内部を確認するためにボックスを開く必要はありません。

命令型の世界では、すべてのボックスの内容を調べて、他のすべてのボックスの内容比較せずに壁を構築するのは危険です。

  • 一部には強力な磁石が含まれており、不適切に配置すると他の磁気ボックスが壁から押し出されます。
  • 一部は非常に高温または低温であり、隣接するスペースに配置すると反応が悪くなります。

ありそうもない比喩であなたの時間を無駄にする前に、私は立ち止まると思いますが、私は要点が述べられていることを望みます。機能的なレンガには隠された驚きはなく、完全に予測可能です。あなたがすることができますので、常に大きないずれかの代わりに右の大きさと形の小さなブロックを使用し、同じ大きさと形状の2つの箱の間に違いはありません、あなたは参照透明性を持っています。必須のレンガでは、適切なサイズと形状を用意するだけでは十分ではありません。レンガがどのように構築されたかを知る必要があります。参照透過的ではありません。

純粋な関数型言語では、関数のシグネチャを確認するだけで、関数の機能を知ることができます。もちろん、内部を調べてパフォーマンスを確認することもできますが、調べる必要はありません。

命令型言語では、どんな驚きが中に隠れるかもしれないか、あなたは決して知りません。


「純粋な関数型言語では、関数のシグネチャを確認するだけで、関数の機能を知ることができます。」–それは一般的には当てはまりません。はい、パラメトリック多型の仮定の下で、我々は種類の機能があると結論することができます(a, b) -> a唯一の可能fst機能と種類の機能があることa -> aのみ可能identity機能がありますが、必ずしもタイプの機能については何も言うことができない(a, a) -> a例えば、。
イェルクWミッターク

2

置換モデル(参照透過性(RT)を使用)を大まかに理解しているので、関数を最も単純な部分に分解できます。式がRTの場合、式を分解して常に同じ結果を得ることができます。

はい、直感はかなり正しいです。以下に、より正確な情報をいくつか紹介します。

あなたが言ったように、どんなRT式もsingle「結果」を持つべきです。つまりfactorial(5)、プログラム内の式が与えられた場合、常に同じ「結果」が得られるはずです。したがって、特定のものfactorial(5)がプログラム内にあり、120を生成する場合、展開または計算される「ステップ順序」に関係なく、時間に関係なく、常に120を生成する必要があります。

例:factorial関数。

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

この説明にはいくつかの考慮事項があります。

まず第一に、異なるRTモデル(適用対通常の順序を参照)は、同じRT式に対して異なる「結果」をもたらす可能性があることに注意してください。

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

上記のコードfirstsecondは、参照は透過的ですが、最後の式は、通常の順序と適用順序で評価された場合、異なる「結果」を生成します(後者では、式は停止しません)。

....引用符で「結果」を使用することになります。式が停止する必要がないため、値が生成されない場合があります。したがって、「結果」を使用することは、ぼやけているようなものです。RT式computationsでは、評価モデルでは常に同じ結果が得られると言えます。

第3 foo(50)に、プログラム内の2つが異なる場所で異なる式として表示されることが必要になる場合があります。それぞれが、互いに異なる独自の結果を生成します。たとえば、言語が動的スコープを許可している場合、両方の式は、字句的には同じですが、異なります。Perlの場合:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

ダイナミックスコープ誤解それは簡単なものを考えるようにのために作るためxの唯一の入力でfoo実際に、それがあるときに、xそしてy。違いを確認する1つの方法は、プログラムを動的スコープなしの同等のプログラムに変換することです。つまり、明示的にパラメーターを渡すためfoo(x)、を定義する代わりに、呼び出し元で明示的に定義foo(x, y)して渡しますy

ポイントは、私たちは常にfunction考え方の下にあります。式に特定の入力が与えられると、対応する「結果」が与えられます。同じ入力を与える場合、常に同じ「結果」を期待する必要があります。

さて、次のコードはどうですか?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

foo再定義があるため、プロシージャはRTを中断します。つまりy、ある時点で定義し、その後、同じよう に再定義しましたy。上記のperlの例では、ysは異なるバインディングですが、同じ文字名「y」を共有しています。ここで、ysは実際には同じです。そのため、(再)割り当てはメタ操作であると言います。実際、プログラムの定義を変更しています。

大まかに言って、人々は通常、その違いを次のように表現しています。副作用のない設定では、からのマッピングがありinput -> outputます。「命令型」設定ではinput -> ouputstate時間の経過とともに変化する可能性があるというコンテキストがあります。

これで、対応する値の代わりに式を使用する代わりに、変換をstate必要とする各操作で変換を適用する必要があります(もちろん、式は同じことを参照してstate計算を実行します)。

したがって、副作用のないプログラムで式を計算するために知っておく必要があるすべてがその個々の入力である場合、命令型プログラムでは、計算ステップごとに入力と状態全体を知る必要があります。推論が最初に大きな打撃を受けます(現在、問題のある手順をデバッグするには、入力コアダンプが必要です)。メモ化のように、特定のトリックは実際的ではありません。しかし、同時に、並行性と並列性ははるかに困難になります。


1
あなたがメモ化について言及するのは素晴らしいことです。これは、外部からは見えない内部状態の例として使用できます。メモ化を使用する関数は、内部で状態とミューテーションを使用していても、参照透過です。
ジョルジオ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.