参照の透明性とは何ですか?


38

命令型パラダイムでそれを見てきました

f(x)+ f(x)

次と同じではない可能性があります。

2 * f(x)

しかし、機能的なパラダイムでは同じでなければなりません。私はPythonとSchemeの両方のケースを実装しようとしましたが、私にとっては同じように見えます。

特定の関数との違いを指摘できる例は何でしょうか?


7
Pythonで参照透過関数を作成できます。違いは、言語がそれを強制しないことです。
カールビーレフェルト14

5
Cでのと似て:f(x++)+f(x++)と同じではないかもしれません2*f(x++)(のようなものが、マクロの中に隠されているときCで、それは特に素敵だ-私はあなたが賭けることに私の鼻を壊したのか?)
ブヨ

私の理解では、@ gnatの例は、Rのような機能指向言語が参照渡しを採用し、引数を変更する関数を明示的に避ける理由です。Rでは、少なくとも、言語の複雑な環境や名前空間、検索パスのシステムを掘り下げることなく、これらの制限を回避することは(少なくとも、安定した、移植可能な方法で)実際には困難です。
シャドウトーカー14

4
@ssdecontrol:実際、参照透過性がある場合、値渡しと参照渡しは常にまったく同じ結果をもたらすため、言語がどちらを使用するかは問題ではありません。関数型言語は、セマンティックを明確にするために値渡しに似たもので指定されることがよくありますが、その実装では、パフォーマンスのために参照渡し(または、両方とも、特定のコンテキストでどちらが速いかによって)を使用することがよくあります。
ヨルグWミットタグ14

4
@gnat:特に、f(x++)+f(x++)未定義の動作を呼び出すため、絶対に何でもかまいません。しかし、それは実際には参照の透過性とは関係ありません。これはこの呼び出しには役に立たないでしょうsin(x++)+sin(x++)。42、ハードドライブをフォーマット、ユーザーの鼻から悪魔が飛び出す可能性があります…
クリストファークロイツィグ14

回答:


62

関数と呼ばれる参照透過性は、引数の値を見るだけでその関数を適用した結果を決定できることを示します。Python、Scheme、Pascal、Cなどの任意のプログラミング言語で参照透過関数を作成できます。

一方、ほとんどの言語では、参照を意識しない透過関数を作成することもできます。たとえば、次のPython関数:

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

参照的に透過的ではなく、実際に呼び出す

foo(x) + foo(x)

そして

2 * foo(x)

任意の引数に対して異なる値を生成しますx。これは、関数がグローバル変数を使用および変更するためです。したがって、各呼び出しの結果は、関数の引数だけでなく、この変化する状態に依存します。

純粋に関数型の言語であるHaskellは、純粋な関数が適用され、常に参照的に透過的である式評価を、参照的に透過的ではないアクション実行(特別な値の処理)から厳密に分離します。つまり、同じアクションを実行するたびに異なる結果。

そのため、Haskell関数については

f :: Int -> Int

および任意の整数x、それは常に真です

2 * (f x) == (f x) + (f x)

アクションの例は、ライブラリ関数の結果ですgetLine

getLine :: IO String

式の評価の結果、この関数(実際は定数)はまずtypeの純粋な値を生成しますIO String。このタイプの値は他の値と同じです。値を渡したり、データ構造に入れたり、特別な関数を使用して構成したりできます。たとえば、次のようなアクションのリストを作成できます。

[getLine, getLine] :: [IO String]

アクションは、Haskellランタイムに次のように記述して実行するよう指示できるという点で特別です。

main = <some action>

この場合、Haskellプログラムが開始されると、ランタイムはバインドされたアクションmain実行し実行し、場合によっては副作用を引き起こします。したがって、同じアクションを2回実行すると、ランタイムが入力として取得するものに応じて異なる結果が生成される可能性があるため、アクションの実行は参照的に透過的ではありません。

Haskellの型システムのおかげで、別の型が期待されるコンテキストではアクションを使用できません。逆の場合も同様です。したがって、文字列の長さを検索する場合は、次のlength関数を使用できます。

length "Hello"

5を返します。ただし、端末から読み取った文字列の長さを検索する場合は、書き込むことができません。

length (getLine)

タイプエラーが発生するためです。lengthタイプリストの入力(および文字列は実際にはリスト)を期待しますが、タイプgetLineの値IO String(アクション)です。このようにして、型システムは、getLine(実行がコア言語の外部で実行され、非参照的に透過的である可能性がある)などのアクション値がtypeの非アクション値内に隠れないようにしますInt

編集

exiztの質問に答えるために、コンソールから行を読み取り、その長さを出力する小さなHaskellプログラムを次に示します。

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

メインアクションは、順次実行される2つのサブアクションで構成されます。

  1. getlineタイプのIO String
  2. 2つ目は、引数putStrLnの型の関数を評価することにより構築されString -> IO ()ます。

より正確には、2番目のアクションは

  1. line最初のアクションによって読み取られた値へのバインド、
  2. 純関数を評価しlength(整数として長さを計算する)、次にshow(整数を文字列に変換する)、
  3. putStrLnの結果に関数を適用してアクションを構築しshowます。

この時点で、2番目のアクションを実行できます。「Hello」と入力した場合、「5」が出力されます。

<-表記法を使用してアクションから値を取得する場合、その値は別のアクション内でのみ使用できることに注意してください。たとえば、次のように書くことはできません。

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

ので、show (length line)型を持つStringDO表記は、(アクションことを必要とするのに対し、getLineタイプのはIO String)別のアクションが続く(例えばputStrLn (show (length line))タイプのIO ())。

編集2

JörgW Mittagの参照の透明性の定義は、私のものよりも一般的です(彼の答えを支持しました)。質問の例は関数の戻り値に焦点を当てているため、制限された定義を使用し、この側面を説明したかったためです。ただし、RTは一般に、プログラム全体の意味を指します。これには、グローバルな状態の変更や、式の評価によって引き起こされる環境(IO)との相互作用が含まれます。したがって、正しい一般的な定義については、その答えを参照する必要があります。


10
downvoterは、この答えを改善する方法を提案できますか?
ジョルジオ14

それでは、Haskellの端末から読み取った文字列の長さをどのように取得するのでしょうか?
sbichenko 14

2
これは非常に教訓的ですが、完全を期すために、アクションと純粋な機能が混在しないことを保証するHaskellの型システムではありません。それは、言語が直接呼び出すことができる不純な機能を提供しないという事実です。HaskellのIO型は、ラムダとジェネリックを備えた任意の言語で実際に非常に簡単にprintln実装できますが、誰でも直接呼び出すことができるため、実装IOは純度を保証しません。それは単なる慣習に過ぎません。
ドーバル

私は、(1)すべての関数が純粋であること(もちろん、言語が不純な関数を提供しないため純粋であることを知っています。不純なアクションにはさまざまなタイプがあるため、混在させることはできません。ところで、直接電話するとはどういう意味ですか?
ジョルジオ

6
getLine参照を透過的にしないというあなたの主張は間違っています。getLine特定の文字列がユーザーの入力に依存する何らかの文字列に評価されるか、それを減らすかのように提示します。これは間違っています。IO Stringに含まれる以上の文字列は含まれませんMaybe StringIO Stringは多分、おそらく文字列を取得するためのレシピであり、式としては、Haskellの他のすべてと同じくらい純粋です。
-LuxuryMode

25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

ただし、それ参照の透明性の意味ではありません。RTとは、プログラムの意味を変更することなく、プログラム内の任意の式をその式の評価結果(またはその逆)に置き換えることができることを意味します。

たとえば、次のプログラムを考えます。

def f(): return 2

print(f() + f())
print(2)

このプログラムは参照的に透過的です。私は1つまたは両方の出現箇所置き換えることができますf()とし2、それはまだ同じように動作します:

def f(): return 2

print(2 + f())
print(2)

または

def f(): return 2

print(f() + 2)
print(2)

または

def f(): return 2

print(2 + 2)
print(f())

すべて同じように動作します。

まあ、実際、私はごまかした。printプログラムの意味を変更せずに、呼び出しをその戻り値(まったく値ではない)に置き換えることができるはずです。ただし、2つのprintステートメントを削除するだけで、プログラムの意味が変わることは明らかです。以前は、何かを画面に出力していましたが、そうではなかったのです。I / Oは参照的に透過的ではありません。

簡単な経験則は次のとおりです。式、部分式またはサブルーチン呼び出しをその式の戻り値に置き換えることができる場合、部分式またはサブルーチン呼び出しはプログラムの任意の場所で、プログラムの意味を変更せずに、参照があります透明性。これが意味することは、実際には、I / Oを持たないこと、可変状態を持たないこと、副作用を持たないことです。すべての式で、式の値は、式の構成部分の値のみに依存する必要があります。また、すべてのサブルーチン呼び出しで、戻り値は引数のみに依存する必要があります。


4
「変更可能な状態にすることはできません」:隠されていて、コードの観察可能な動作に影響を与えない場合は、変更することができます。たとえばメモ化について考えてください。
ジョルジオ14

4
@Giorgio:これはおそらく主観的ですが、キャッシュされた結果は、隠されていて観察可能な効果がない場合、実際には「可変状態」ではないと主張します。不変性は常に、可変ハードウェア上に実装される抽象化です。多くの場合、言語によって提供されます(実行中に値がレジスタとメモリの場所間を移動できる場合でも「値」の抽象化を提供し、再び使用されることはないことが判明すると消滅する可能性があります)ライブラリなどによって提供されます。(もちろん、正しく実装されていると仮定します。)
ruakh 14

1
+1私はprint例が本当に好きです。おそらくこれを確認する1つの方法は、画面に印刷されるものが「戻り値」の一部であることです。print関数の戻り値と同等のターミナルでの書き込みで置き換えることができる場合、この例は機能します。
ピエールアラード14

1
@Giorgio空間/時間の使用は、参照の透明性を目的とした副作用とは見なされません。実行時間が異なるため、互換性がなく42 + 2互換性がありません。また、参照の透明性の全体的なポイントは、式を評価するもので置き換えることができるということです。重要な考慮事項は、スレッドの安全性です。
ドーバル14

1
@overexchange:参照の透明性とは、プログラムの意味を変えることなく、すべての部分式をその値で置き換えることができることを意味します。listOfSequence.append(n)リターンはNone、あなたがにすべてのコールを交換することができるはずlistOfSequence.append(n)None、あなたのプログラムの意味を変えずに。できますか?そうでない場合、参照的に透過的ではありません。
ヨルグWミットタグ

1

この回答の一部は、私のGitHubアカウントでホストされている、機能プログラミングに関する未完成のチュートリアルから直接引用されています。

関数は、同じ入力パラメーターが与えられ、常に同じ出力(戻り値)を生成する場合、参照透過と呼ばれます。純粋な関数型プログラミングの存在理由を探しているなら、参照の透明性は良い候補です。代数、算術、および論理の式を使用して推論する場合、このプロパティ(同等の同等の代替性とも呼ばれます)は基本的に重要であるため、通常は当然のことと見なされます...

簡単な例を考えてみましょう。

x = 42

純粋な関数型言語では、等号の左側と右側は両方の方法で相互に置換可能です。つまり、Cのような言語とは異なり、上記の表記法は真に平等を主張します。その結果、数学コードと同じようにプログラムコードについて推論することができます。

Haskell wikiから:

純粋な計算は、呼び出されるたびに同じ値を生成します。このプロパティは参照透過性と呼ばれ、コードの等式推論を行うことを可能にします...

これとは対照的に、Cライクな言語によって実行される操作のタイプは、破壊的な割り当てと呼ばれることもあります。

純粋という用語は、この議論に関連する表現の特性を記述するためによく使用されます。関数が純粋と見なされるには、

  • 副作用を示すことは許可されていません。
  • 参照的に透明でなければなりません。

多くの数学の教科書に見られるブラックボックスのメタファーによれば、関数の内部は外の世界から完全に封印されています。副作用とは、関数または式がこの原則に違反する場合です。つまり、プロシージャが何らかの方法で他のプログラムユニットと通信することを許可します(情報の共有と交換など)。

要約すると、プログラミング言語のセマンティクスでも、関数がtrueのように動作するためには、参照透過性が必須です。


これはここから取られた単語ごとのコピーで開くようです:「同じ入力パラメータが与えられ、常に同じ出力を生成する場合、関数は参照的に透明であると言われます...」Stack Exchangeには盗作のルールがありますこれらについて知っていますか?「Plagiarismは、他の誰かの作品の塊をコピーし、自分の名前を平手打ちし、自分自身を元の著者として渡すという魂のない行為です...」
gnat 14

3
そのページを書きました。
yesthisisuser

この場合、盗作のように見えないようにすることを検討してください-読者には伝える方法がないためです。SEでこれを行う方法を知っていますか?1)「As(I have)written [here](link to source)...」のようなオリジナルのソースを参照し、その後に2)適切な引用フォーマット(引用符、またはそれ以上の> 記号を使用)を続けます。一般的な指針を与えることに加えて、解答アドレス具体的な質問がについて尋ねた場合にも、およそこの場合には、損はないf(x)+f(x)/ 2*f(x)、見に答えるためにどのようにそれ以外の場合は、あなたは、単にあなたのページを宣伝しているように見えるかもしれ-
ブヨ

1
理論的には、この答えを理解しました。しかし、実際にはこれらのルールに従って、私はこのプログラムであられのシーケンスリストを返す必要があります。どうすればいいですか?
15
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.