什么是参照透明性?


38

我已经看到在命令式范式中

f(x)+ f(x)

可能与以下内容不同:

2 * f(x)

但是在功能范式中应该相同。我曾尝试在Python和Scheme中实现这两种情况,但对我来说,它们看起来非常简单。

有什么例子可以指出与给定功能的区别?


7
您可以并且经常这样做,使用python编写参照透明的函数。区别在于语言不强制执行。
Karl Bielefeldt 2014年

5
在C等中:f(x++)+f(x++)可能与2*f(x++)(在C中,当类似的东西隐藏在宏中时特别可爱-我是否为此broke过鼻子?您敢打赌)
gna

以我的理解,@ gnat的示例就是为什么像R这样的面向功能的语言采用传递引用,并明确避免使用修改其参数的函数。至少在R语言中,如果不深入研究语言复杂的环境,名称空间和搜索路径,实际上很难(至少以一种稳定,可移植的方式)规避这些限制。
shadowtalker 2014年

4
@ssdecontrol:实际上,当您具有引用透明性时,按值传递和按引用传递总是会产生完全相同的结果,因此,使用哪种语言都没有关系。功能语言通常以类似于值传递的方式指定,以实现语义清晰,但是它们的实现通常使用传递引用来提高性能(或什至两者,这取决于给定上下文中哪种更快)。
约尔格W¯¯米塔格

4
@gnat:特别地,f(x++)+f(x++)绝对可以是任何东西,因为它正在调用未定义的行为。但这与引用透明性并没有真正的关系-对此调用无济于事,对于引用透明性函数(如中)sin(x++)+sin(x++),它也是“未定义的” 。可能是42岁,可以格式化硬盘,可以让恶魔从用户的鼻子里飞出来……
Christopher Creutzig 2014年

Answers:


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执行该动作,可能会产生副作用。因此,动作执行不是参照透明的,因为两次执行相同的动作会产生不同的结果,具体取决于运行时输入的内容。

得益于Haskell的类型系统,操作永远不能在需要其他类型的上下文中使用,反之亦然。因此,如果要查找字符串的长度,可以使用以下length函数:

length "Hello"

将返回5。但是,如果您要查找从终端读取的字符串的长度,则无法写

length (getLine)

因为您收到类型错误:length期望输入类型为list(而字符串实际上是一个列表),但它getLine是类型的值IO String(动作)。这样,类型系统确保不会将type getLine(的执行值在核心语言之外执行,并且可能是非参照透明的)动作值隐藏在type的非动作值中Int

编辑

为了回答突出问题,这是一个小的Haskell程序,该程序从控制台读取一行并打印其长度。

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

主要动作由两个子动作组成,这些子动作顺序执行:

  1. getlineIO String
  2. 第二种是通过评估其参数上putStrLn的type 函数来构造的String -> IO ()

更确切地说,第二个动作是由

  1. 绑定line到第一个操作读取的值,
  2. 评估纯函数length(将长度计算为整数),然后show(将整数转换为字符串),
  3. 通过将函数应用putStrLn到结果来建立动作show

此时,可以执行第二个动作。如果您键入“ Hello”,它将打印“ 5”。

请注意,如果使用<-表示法从某个操作中获取一个值,则只能在另一个操作中使用该值,例如,您不能编写:

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

因为show (length line)具有类型,String而do表示法则要求一个操作(getLine类型IO String)后面跟随另一个操作(例如putStrLn (show (length line))类型IO ())。

编辑2

约尔格·米塔格(JörgW Mittag)对参照透明的定义比我的定义更为笼统(我赞成他的回答)。我使用了一个受限制的定义,因为问题中的示例着重于函数的返回值,并且我想说明这一方面。但是,RT通常是指整个程序的含义,包括对全局状态的更改以及由于评估表达式而导致的与环境(IO)的交互。因此,对于正确的一般定义,您应该参考该答案。


10
拒绝投票的人可以建议我如何改善此答案吗?
Giorgio

那么如何从Haskell的终端中读取一个字符串的长度呢?
sbichenko 2014年

2
这是非常古怪的,但是为了完整起见,并不是Haskell的类型系统可以确保操作和纯函数不会混合;这是该语言没有提供任何可以直接调用的不纯函数的事实。实际上,您可以IO使用lambda和泛型在任何语言中轻松实现Haskell的类型,但是由于任何人都可以println直接调用,因此实现IO并不能保证纯净。这仅仅是一个约定。
Doval

我的意思是(1)所有函数都是纯函数(当然,它们是纯函数,因为据我所知,有一些绕过该机制的机制,尽管该语言没有提供任何不纯函数),以及(2)纯函数和不纯行为的类型不同,因此不能混在一起。顺便说一句,您直接打电话是什么意思?
Giorgio 2014年

6
关于getLine不具有参照透明性的观点是错误的。您所呈现的getLine好像它评估或简化为某个String一样,其中的特定String取决于用户的输入。这是不正确的。IO String包含的字符串Maybe String不超过字符串。IO String是一个配方,可能会获得一个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)

该程序是参照透明的。我可以用替换一个或两个出现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用其返回值(根本没有值)替换对的调用,而无需更改程序的含义。但是,很明显,如果我只删除这两个print语句,程序的含义将发生变化:之前,它在屏幕上打印了一些内容,之后则没有。I / O不是参照透明的。

简单的经验法则是:如果可以在程序中的任何位置用该表达式,子表达式或子例程的返回值替换任何表达式,子表达式或子例程的调用,而无需更改程序的含义,则您具有引用透明度。实际上,这意味着您不能拥有任何I / O,不能具有任何可变状态,不能具有任何副作用。在每个表达式中,表达式的值必须仅取决于表达式组成部分的值。并且在每个子例程调用中,返回值必须仅取决于参数。


4
“不能有任何可变状态”:好吧,如果它是隐藏的并且不影响代码的可观察行为,则可以拥有它。想想例如记忆。
Giorgio 2014年

4
@Giorgio:这也许是主观的,但是我认为如果缓存的结果是隐藏的并且没有可观察的效果,则它们并不是真正的“可变状态”。不变性始终是在可变硬件之上实现的抽象。它通常是由语言提供的(给出“一个值”的抽象,即使该值可以在执行期间在寄存器和内存位置之间移动,并且一旦知道它将不再使用就可以消失),但是当它被使用时,它同样有效由图书馆或其他机构提供。(假设它的正确实施,当然,。)
ruakh

1
+1我真的很喜欢这个print例子。也许看到这种情况的一种方法是,屏幕上打印的内容是“返回值”的一部分。如果可以print用其函数返回值和在终端上的等效文字替换,则该示例有效。
皮埃尔·阿洛德

1
@Giorgio出于参考透明的目的,不能将时空使用视为副作用。这将使42 + 2非可互换的,因为它们具有不同的运行时间,并引用透明的整点是,你可以替换与任何评估为表达式。重要的考虑因素是线程安全性。
2014年

1
@overexchange:引用透明性意味着您可以将每个子表达式替换为其值,而无需更改程序的含义。listOfSequence.append(n)回报None,所以你应该能够每次调用替换到listOfSequence.append(n)None没有改变你的程序的含义。你能做到吗?如果不是,则它不是参照透明的。
约尔格W¯¯米塔格

1

这个答案的一部分直接取自我的GitHub帐户上托管的未完成的函数式编程教程

如果函数在给定相同的输入参数的情况下始终产生相同的输出(返回值),则称该函数为参照透明的。如果正在寻找纯函数式编程的理由,那么引用透明性是一个很好的选择。当用代数,算术和逻辑中的公式进行推理时,此属性(也称为“等于等于”的替代性)从根本上非常重要,因此通常认为这是理所当然的……

考虑一个简单的例子:

x = 42

在纯函数式语言中,等号的左手边和右手边可通过两种方式彼此替代。也就是说,与诸如C之类的语言不同,上述表示法确实声明了相等性。这样的结果是我们可以像数学方程式一样推理程序代码。

Haskell Wiki

每次调用时,纯计算都会产生相同的值。此属性称为参照透明性,可以对代码进行方程式推理

与此相反,有时将由类C语言执行的操作类型称为破坏性分配

术语“ 纯”通常用于描述与该讨论相关的表达式的属性。对于被认为是纯函数,

  • 不允许出现任何副作用,并且
  • 它必须是参照透明的。

根据许多数学教科书中发现的黑匣子隐喻,函数的内部与外界完全隔离。副作用是函数或表达式违反此原理时-即,允许该过程以某种方式与其他程序单元进行通信(例如,共享和交换信息)。

总而言之,在程序设计语言的语义中,引用透明性对于使函数的行为像true一样是必须的,数学函数也是如此


这似乎是从这里取来的逐字逐字打开的:“如果给定相同的输入参数,该函数始终产生相同的输出,则该函数被称为参照透明的。” Stack Exchange有窃的规则,你知道这些吗?“ P窃是无情的行为,它复制了别人的作品,在名字上打上你的名字,并以原作者的身份
转嫁给

3
我写了那页。
yesthisisuser 2014年

如果是这种情况,请考虑使其看起来不像是窃-因为读者无法分辨。您知道如何在SE进行此操作吗?1)引用原始来源,例如“按原样写[here](link to source)...”,然后引用2)正确的引号格式(使用引号或更佳的> 符号表示)。它也不会伤害,如果除了提供一般指导,回答地址具体问题问,在这种情况下大约f(x)+f(x)/ 2*f(x),请参阅如何回答 -否则它可能看起来像你只是广告您的网页
蚊蚋

1
从理论上讲,我理解这个答案。但是,实际上遵循这些规则,我需要在该程序中返回冰雹序列列表。我该怎么做呢?
汇率过高的情况,2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.