参考透明度一词是什么意思?我听说它被描述为“这意味着你可以用 equals 替换 equals”,但这似乎是一个不充分的解释。
术语“参照透明性”来自analytical philosophy,这是一个基于逻辑和数学方法分析自然语言结构、陈述和论证的哲学分支。换句话说,它是计算机科学之外最接近我们所说的 programming language semantics 的学科。哲学家 Willard Quine 负责发起参照透明性的概念,但它也隐含在 Bertrand Russell 和 Alfred Whitehead 的方法中。
“参照透明”的核心是一个非常简单明了的概念。 “所指”一词在分析哲学中用于谈论表达式所指的事物。它与我们在编程语言语义中所说的“意义”或“外延”大致相同。使用 Andrew Birkett 的示例 (blog post),术语“苏格兰首都”指的是爱丁堡市。这是“参照物”的一个简单示例。
如果用引用同一实体的另一个术语替换该上下文中的术语不会改变含义,则句子中的上下文是“引用透明的”。例如
苏格兰议会在苏格兰首都开会。
意思是一样的
苏格兰议会在爱丁堡举行会议。
所以上下文“苏格兰议会在……开会”是一个参照透明的上下文。我们可以将“苏格兰的首都”替换为“爱丁堡”而不改变其含义。换句话说,上下文只关心该术语所指的内容,而不关心其他内容。这就是上下文“参照透明”的意义。
另一方面,在句子中,
爱丁堡自 1999 年以来一直是苏格兰的首都。
我们不能做这样的替换。如果我们这样做了,我们会得到“Edinburgh has been Edinburgh since 1999”,这是一个疯狂的说法,并且与原始句子的含义不同。因此,“爱丁堡自 1999 年以来一直是……”的上下文似乎在引用上是不透明的(与引用透明相反)。它显然关心的不仅仅是这个词所指的东西。它是什么?
诸如“苏格兰的首都”之类的东西被称为确定的术语,它们在很长一段时间内都没有让逻辑学家和哲学家感到头疼。 Russell 和 Quine 将它们整理出来,说它们实际上并不是“指代”,即认为上面的例子用来指代实体是错误的。理解“爱丁堡自 1999 年以来一直是苏格兰的首都”的正确方式是说
苏格兰自 1999 年以来就有了首都,首都是爱丁堡。
这句话不能变成一个疯狂的句子。问题解决了! Quine 的观点是说自然语言是混乱的,或者至少是复杂的,因为它是为了方便实际使用而设计的,但哲学家和逻辑学家应该通过以正确的方式理解它们来使它们变得清晰。参照透明性是一种用于使含义清晰的工具。
这一切与编程有什么关系?实际上,不是很多。正如我们所说,参照透明性是一种用于理解语言的工具,即分配含义。 Christopher Strachey,他创立了编程语言语义领域,在他的意义研究中使用了它。他的基础论文“Fundamental concepts in programming languages”可在网络上找到。这是一篇漂亮的论文,每个人都可以阅读和理解它。所以,请这样做。你会大开眼界。他在本段中介绍了“参照透明度”一词:
表达式最有用的属性之一是 Quine 引用透明性所调用的属性。从本质上讲,这意味着如果我们希望找到包含子表达式的表达式的值,我们唯一需要知道的关于子表达式的就是它的值。子表达式的任何其他特征,例如其内部结构、其组成部分的数量和性质、它们的评估顺序或书写它们的墨水颜色,都与主表达式的值无关表达。
“本质上”的使用表明 Strachey 是在对其进行释义,以便用简单的术语来解释它。函数式程序员似乎以他们自己的方式理解这一段。论文中还有其他 9 处“引用透明性”,但它们似乎并不关心其他任何一个。事实上,Strachey 的整篇论文都致力于解释命令式编程语言的含义。但是,今天,函数式程序员声称命令式编程语言不是引用透明的。 Strachey 将在他的坟墓里翻身。
我们可以挽救局面。我们说自然语言是“杂乱无章的,或者至少是复杂的”,因为它是为了方便实际使用而设计的。编程语言也是如此。它们是“杂乱无章的,或者至少是复杂的”,因为它们被制作成便于实际使用。这并不意味着他们需要混淆我们。他们只需要以正确的方式被理解,使用一种引用透明的元语言,这样我们就有了清晰的含义。在我引用的论文中,Strachey 正是这样做的。他通过将命令式编程语言分解为基本概念来解释它们的含义,并且在任何地方都不会失去清晰度。他分析的一个重要部分是指出编程语言中的表达式有两种“值”,称为左值和右值。在 Strachey 的论文之前,人们不理解这一点,混乱占了上风。今天,C 的定义经常提到它,每个 C 程序员都明白其中的区别。 (其他语言的程序员是否同样理解它很难说。)
Quine 和 Strachey 都关心涉及某种形式的语境依赖的语言结构的意义。例如,我们的示例“爱丁堡自 1999 年以来一直是苏格兰的首都”表示“苏格兰的首都”取决于考虑它的时间这一事实。这种上下文依赖性在自然语言和编程语言中都是现实的。即使在函数式编程中,自由和绑定变量也要根据它们出现的上下文来解释。任何类型的上下文依赖都会以某种方式阻止引用透明性。如果你试图理解术语的含义而不考虑它们所依赖的上下文,你会再次陷入混乱。奎因关注模态逻辑的含义。他认为 modal logic 在引用上是不透明的,应该通过将其转换为引用透明的框架来清理它(例如,通过将必要性视为可证明性)。他在很大程度上输掉了这场辩论。逻辑学家和哲学家都发现克里普克的可能世界语义是完全足够的。命令式编程也存在类似的情况。 Strachey 解释的状态依赖和 Reynolds 解释的存储依赖(以类似于 Kripke 的可能世界语义的方式)是完全足够的。函数式程序员对这项研究了解不多。他们关于参照透明度的想法是不可取的。
[补充说明:上面的例子说明了一个简单的短语,如“苏格兰的首都”,具有多个层次的含义。在一个层面上,我们可能正在谈论当前的资本。在另一个层面上,我们可能会谈论苏格兰在一段时间内可能拥有的所有可能的首都。在正常实践中,我们可以很容易地“放大”特定的上下文并“缩小”以跨越所有上下文。自然语言的效率利用了我们这样做的能力。命令式编程语言以非常相似的方式高效。我们可以使用赋值右侧的变量 x(r 值)来讨论它在特定状态下的值。或者,我们可以谈论它跨越所有状态的 l 值。人们很少对这些事情感到困惑。然而,它们可能也可能无法准确解释语言结构中固有的所有含义层。所有这些层次的意义不一定是“显而易见的”,正确研究它们是科学问题。但是,普通人对这种分层含义的解释不清晰,并不意味着他们对此感到困惑。]
下面一个单独的“后记”将这个讨论与函数式和命令式编程的关注点联系起来。
引用透明性是函数式编程中常用的一个术语,意味着给定一个函数和一个输入值,您将始终收到相同的输出。也就是说函数中没有使用外部状态。
下面是一个引用透明函数的例子:
int plusOne(int x)
{
return x+1;
}
对于引用透明函数,给定一个输入和一个函数,您可以用一个值替换它而不是调用该函数。因此,我们可以将其替换为 6,而不是使用参数 5 调用 plusOne。
另一个很好的例子是一般的数学。在数学中,给定一个函数和一个输入值,它总是映射到相同的输出值。 f(x) = x + 1。因此数学中的函数是参照透明的。
这个概念对研究人员很重要,因为它意味着当您拥有一个引用透明的函数时,它可以轻松实现自动并行化和缓存。
引用透明性总是在像 Haskell 这样的函数式语言中使用。
--
相比之下,存在参照不透明性的概念。这意味着相反。调用函数可能并不总是产生相同的输出。
//global G
int G = 10;
int plusG(int x)
{//G can be modified externally returning different values.
return x + G;
}
另一个例子是面向对象编程语言中的成员函数。成员函数通常对其成员变量进行操作,因此引用不透明。成员函数当然可以是引用透明的。
另一个示例是从文本文件读取并打印输出的函数。这个外部文本文件可以随时更改,因此该函数在引用上是不透明的。
引用透明函数是仅依赖于其输入的函数。
[这是我 3 月 25 日回答的附言,旨在使讨论更接近函数式/命令式编程的关注点。]
函数式程序员的引用透明性的想法似乎在三个方面与标准概念不同:
哲学家/逻辑学家使用“reference”、“denotation”、“designatum”和“bedeutung”(弗雷格的德语术语)等术语,而函数式程序员使用“value”一词。 (这不完全是他们做的。我注意到,Landin、Strachey 和他们的后代也使用“价值”一词来谈论指称/外延。这可能只是 Landin 和 Strachey 引入的术语简化,但它似乎做了一个以幼稚的方式使用时有很大的不同。)
函数式程序员似乎相信这些“价值”存在于编程语言内部,而不是外部。在这样做时,他们不同于哲学家和编程语言语义学家。
他们似乎认为这些“价值”应该是通过评估获得的。
例如,今天早上 referential transparency 上的 Wikipedia 文章说:
如果一个表达式可以用它的值替换而不改变程序的行为(换句话说,产生一个在相同输入上具有相同效果和输出的程序),则称该表达式是引用透明的。
这与哲学家/逻辑学家所说的完全不同。他们说,如果上下文中的表达式可以被另一个引用同一事物的 表达式 替换(coreferential 表达式),则该上下文是引用或引用透明的。这些哲学家/逻辑学家是谁?它们包括 Frege、Russell、Whitehead、Carnap、Quine、Church 和无数其他。他们每一个人都是一个巍峨的身影。这些逻辑学家的综合智力至少可以说是惊天动地的。他们都一致认为,指称/外延存在于形式语言之外,语言内的表达只能谈论它们。因此,在该语言中所能做的就是将一个表达式替换为另一个引用同一实体的表达式。指称/指称本身不存在于语言中。为什么函数式程序员会偏离这个既定的传统?
人们可能会认为编程语言语义学家可能误导了他们。但是,他们没有。
(a) 每个表达式都有一个嵌套的子表达式结构,(b) 每个子表达式表示某些东西(通常是一个数字、真值或数值函数),(c) 一个表达式表示的东西,即它的“值”,仅取决于它的子表达式的值,而不是它们的其他属性。 [补充强调]
Stoy:
表达式唯一重要的是它的值,并且任何子表达式都可以替换为任何其他值相等的[强调]。此外,表达式的值在一定范围内,无论何时出现都是相同的”。
表达式的值仅取决于其组成表达式(如果有的话)的值,并且这些子表达式可以被具有相同值的其他表达式自由替换[强调]。
因此,回想起来,Landin 和 Strachey 通过用“价值”代替“参考”/“外延”来简化术语的努力可能是不明智的。一旦听到“价值”,就会很容易想到导致它的评估过程。将评估产生的任何东西视为“价值”同样很诱人,尽管很明显这不是外延。这就是我收集到的函数式程序员眼中的“引用透明”概念所发生的事情。但是早期语义学家所说的“价值”不是评估的结果,也不是函数的输出或任何类似的东西。这是术语的外延。
一旦我们将一个表达式的所谓“价值”(古典哲学家话语中的“指称”或“外延”)理解为一个复杂的数学/概念对象,各种可能性就会打开。
Strachey 将命令式编程语言中的变量解释为 L 值,正如我在 3 月 25 日的回答中提到的,这是一个复杂的概念对象,在编程语言的语法中没有直接表示。
他还解释了诸如状态到状态函数之类的语言中的命令,这是复杂数学对象的另一个实例,它不是语法中的“值”。
即使是 C 中的副作用函数调用也有一个明确定义的“值”,作为将状态映射到状态和值对的状态转换器(函数式程序员术语中的所谓“monad”)。
函数式程序员不愿将此类语言称为“引用透明”仅意味着他们不愿承认此类复杂的数学/概念对象为“值”。另一方面,他们似乎非常愿意将状态转换器称为“值”,当它被放入他们自己喜欢的语法并用像“monad”这样的流行词修饰时。我不得不说他们完全不一致,即使我们承认他们的“参考透明度”想法有一些连贯性。
一些历史可能会对这些混淆是如何产生的有所了解。 1962 年至 1967 年这段时间对克里斯托弗·斯特拉奇来说是一个非常密集的时期。 1962-65 年间,他在 Maurice Wilkes 担任兼职研究助理,负责设计和实现后来被称为 CPL 的编程语言。这是一种命令式编程语言,但也意味着具有强大的函数式编程语言功能。 Landin 是 Strachey 咨询公司的一名员工,对 Strachey 对编程语言的看法产生了巨大影响。在 1965 年具有里程碑意义的论文“Next 700 programming languages”中,Landin 毫不掩饰地推广函数式编程语言(称它们为外延语言)并将命令式编程语言描述为它们的“对立面”。在随后的讨论中,我们发现 Strachey 对 Landin 的强势地位提出了质疑。
... DL 构成所有语言的子集。它们是一个有趣的子集,但除非你习惯它,否则使用起来很不方便。我们需要它们,因为目前我们不知道如何使用包括命令式和跳转在内的语言构建证明。 [补充强调]
1965 年,Strachey 担任牛津大学的一名读者,似乎基本上全职致力于发展命令式和跳跃理论。到 1967 年,他准备好了一个理论,并在哥本哈根暑期学校的“Fundamental concepts in programming languages”课程中教授了该理论。讲义本应该发表,但“不幸的是,由于编辑拖沓,论文集从未实现;然而,与 Strachey 在牛津的大部分工作一样,该论文在私人发行量上颇具影响力。” (Martin Campbell-Kelly)
获得 Strachey 著作的困难可能导致混乱被传播,人们依赖二手资料和传闻。但是,既然“Fundamental concepts”在网络上很容易获得,就没有必要依靠猜测工作了。我们应该阅读它并自己决定 Strachey 的意思。尤其是:
在第 3.2 节中,他处理“表达式”,其中他谈到了“R 值引用透明度”。
他的第 3.3 节涉及“命令”,其中他谈到了“L 值引用透明度”。
在第 3.4.5 节中,他谈到了“函数和例程”,并声明“在 R 值上下文中 R 值引用透明度的任何偏离都应该通过将表达式分解为多个命令和更简单的表达式来消除,或者,如果事实证明这很困难,这是评论的主题。”
在不理解 L 值、R 值和填充命令式程序员的概念世界的其他复杂对象之间的区别的情况下,任何谈论“引用透明度”都是根本错误的。
如果一个表达式可以用它的值替换,而不改变算法,则表达式是引用透明的,产生一个在相同输入上具有相同效果和输出的算法。
引用透明函数是一种类似于数学函数的函数。给定相同的输入,它总是会产生相同的输出。这意味着传入的状态没有被修改,并且函数没有自己的状态。
对于那些需要简明解释的人,我会冒险(但请阅读下面的披露)。
编程语言中的参照透明性促进了等式推理——参照透明性越高,执行等式推理就越容易。例如,使用(伪)函数定义,
fx = x + x,
在此定义的范围内,您可以(安全地)将 f(foo) 替换为 foo + foo 的容易程度,而无需对可以执行此缩减的位置有太多限制,这很好地表明了您的编程语言的引用透明度有。
例如,如果 foo 在 C 编程意义上是 x++,那么您将无法安全地执行此归约(也就是说,如果您要执行此归约,您最终不会得到与开始时相同的程序)。
在实际的编程语言中,您不会看到完美的引用透明性,但函数式程序员比大多数人更关心它(参见 Haskell,它是一个核心目标)。
(完全披露:我是一名函数式程序员,所以按照最重要的答案,你应该对这个解释持保留态度。)
指称语义基于建模语言,通过构建构成可指称值的域。函数式程序员使用术语值来描述基于语言重写规则的计算的收敛性,即。其操作语义。
在 1 中有两种语言的明确性:
被建模的对象语言
建模语言,元语言
2,由于对象和元语言的接近,它们可能会被混淆。
作为一名语言实现者,我发现我需要不断地记住这种区别。
所以雷迪教授可以这样解释你:-)
在函数式编程和语义的上下文中,术语参照透明不是参照透明的。
我希望以下答案可以补充和限定有争议的第一个和第三个答案。
让我们承认一个表达式表示或指代某个指称对象。然而,一个问题是这些指示物是否可以同构地编码为表达式本身的一部分,称这些表达式为“值”。例如,文字数值是算术表达式集的子集,真值是布尔表达式集的子集,等等。这个想法是评估一个表达式的值(如果它有一个)。因此,“价值”这个词可能指代指称或一组表达式中的一个显着元素。但是,如果所指对象和值之间存在同构(双射),我们可以说它们是同一事物。 (这就是说,必须小心定义指称和同构,正如指称语义领域所证明的那样。举一个对第三个答案的答复中提到的例子,代数数据类型定义 data Nat = Zero | Suc Nat
与预期的不对应到自然数集。)
让我们将 E[·]
写成带有孔的表达式,在某些方面也称为“上下文”。类 C 表达式的两个上下文示例是 [·]+1
和 [·]++
。
让我们将 [[·]]
表示为接受表达式(无孔)并在某个提供意义的宇宙中传递其意义(指称、外延等)的函数。 (我从指称语义学领域借用符号。)
让我们稍微正式地修改 Quine 的定义如下:上下文 E[·]
是指代透明的,当且仅当给定任何两个表达式 E1
和 E2
(那里没有漏洞)使得 [[E1]] = [[E2]]
(即表达式表示/引用相同的所指对象),那么 [[E[E1]]] = [[E[E2]]]
就是这种情况(即用 E1
或 E2
填充孔会导致表达式也表示相同的所指对象)。
Leibniz 用equals 代替equals 的规则通常表示为“if E1 = E2
then E[E1] = E[E2]
”,即E[·]
是一个函数。函数(或就此而言,计算函数的程序)是从源到目标的映射,因此每个源元素至多有一个目标元素。非确定性函数用词不当,它们要么是关系,要么是传递集合的函数,等等。如果在莱布尼茨规则中,等式 =
是指称的,那么双括号就被认为是理所当然的并被省略了。所以引用透明的上下文是一个函数。而莱布尼茨规则是等式推理的主要成分,所以等式推理肯定与参照透明有关。
虽然 [[·]]
是从表达式到外延的函数,但它可以是从表达式到“值”的函数,理解为表达式的受限子集,而 [[·]]
可以理解为求值。
现在,如果 E1
是一个表达式,而 E2
是一个值,那么我认为大多数人在定义表达式、值和评估方面的引用透明性时的意思。但正如本页第一个和第三个答案所示,这是一个不准确的定义。
[·]++
等上下文的问题不是副作用,而是它的值在 C 中没有与其含义同构地定义。函数不是值(好吧,指向函数的指针是),而在函数式编程语言中它们是。 Landin、Strachey 和指称语义学的先驱们在使用功能世界来提供意义方面非常聪明。
对于命令式类 C 语言,我们可以(大致)使用函数 [[·]] : Expression -> (State -> State x Value)
为表达式提供语义。
Value
是 Expression
的子集。 State
包含对(标识符,值)。语义函数接受一个表达式,并将一个函数从当前状态传递给具有更新状态和一个值的对。例如,[[x]]
是从当前状态到第一个分量是当前状态,第二个分量是 x 值的对的函数。相反,[[x++]]
是从当前状态到第一个组件是 x 的值递增的状态,第二个组件是那个值的对的函数。从这个意义上说,上下文 [·]++
是参照透明的,如果它满足上面给出的定义。
我认为函数式程序员有权使用引用透明性,因为他们自然地将 [[·]]
作为函数从表达式恢复到值。函数是一等值,状态也可以是一个值,而不是一个外延。状态单子(部分)是一种用于传递(或线程化)状态的干净机制。
当我阅读接受的答案时,我以为我在另一个页面上,而不是在 stackoverflow 上。
引用透明性是定义纯函数的一种更正式的方式。因此,如果一个函数在相同的输入上始终产生相同的结果,则称它是引用透明的。
let counter=0
function count(){
return counter++
}
这不是引用透明的,因为返回值取决于外部变量“counter”并且它不断变化。
这就是我们如何使其引用透明:
function count(counter){
return counter+1
}
现在这个函数是稳定的,并且在提供相同的输入时总是返回相同的输出。
请注意,这个“意义”的概念是在观察者的脑海中发生的。因此,相同的“参考”对不同的人可能意味着不同的东西。因此,例如,我们在维基百科中有一个爱丁堡消歧页面。
可能出现在编程上下文中的一个相关问题可能是多态性。
也许我们应该为多态的特殊情况(甚至可能是强制转换)命名,其中不同的多态情况在语义上是等价的(而不仅仅是相似。例如,数字 1 - 可以表示使用整数类型、复杂类型或各种其他类型中的任何一种——可以多态处理)。
参照透明性可以简单地表述为:
一个表达式在任何上下文 [1] 中总是计算相同的结果,
一个函数,如果给定两次相同的参数,则必须两次产生相同的结果 [2]。
例如,编程语言 Haskell 是一种纯函数式语言;意味着它是参照透明的。
参照透明性是计算机科学中使用的一个术语。它起源于数理逻辑,但它在计算机科学中具有广泛的应用和有效的意义。
它的意思是:可以被其结果替换而不改变其含义的构造(例如函数)。
在一般使用中,它与纯表达式相似,但并不完全等价。纯表达式仅由其他纯表达式组成。引用透明的表达式可以是内部不纯的,例如在其计算过程中使用可变状态,但在整个表达式之外没有副作用。
所有纯函数,由于它们的构造,在引用上是透明的,但不一定反之亦然。
许多语言特性都支持不纯的引用透明性,例如 Haskell 中的 ST
monad,以及 C++ 中的 constexpr
和某些 lambda。
有时会强制执行引用透明性,而有时程序员必须自己保证。