引用透明性,指的是一个函数,表示您只能通过查看其参数的值来确定应用该函数的结果。您可以使用任何编程语言(例如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))
主要动作由两个子动作组成,这些子动作顺序执行:
getline
型IO String
,
- 第二种是通过评估其参数上
putStrLn
的type 函数来构造的String -> IO ()
。
更确切地说,第二个动作是由
- 绑定
line
到第一个操作读取的值,
- 评估纯函数
length
(将长度计算为整数),然后show
(将整数转换为字符串),
- 通过将函数应用
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)的交互。因此,对于正确的一般定义,您应该参考该答案。