无国籍意味着参照透明度?



我有这个代码

data Slist a = Empty | Scons (Sexp a) (Slist a) 
data Sexp a = AnAtom a | AnSlist (Slist a)
data Fruit = Peach | Apple | Pear | Lemon | Fig deriving (Show,Eq)
sxOccurs oatm sxp =
let slOC Empty = 0
slOC (Scons se sls) = (seOC se) + (slOC sls)
seOC (AnAtom atm) = if (atm == oatm) then 1 else 0
seOC (AnSlist sla) = slOC sla
in seOC sxp

正如你在sxOccurs中看到的,我在let中有两个辅助函数,正如我的小MLer所说的那样,它们是"相互自我引用的":slOCseOC。因此,在 SML 中,您必须使用关键字and让他们相互了解并相互"交叉引用"。顺便说一句,sxOccurs计算 s 列表中特定AnAtom对象的数量,在我的示例中,原子Fruit变量。

我的问题是,这是参考透明度的一个例子吗?同样,在戴维身上,他举了这个例子。

let s0 = emptyStack
s1 = push 12.2 s0
s2 = push 7.1 s1
s3 = push 6.7 s2
s4 = divStack s3
s5 = push 4.3 s4
s6 = subtStack s5
s7 = multStack s6
s8 = push 2.2 s7
s9 = addStack s8
in popStack s9

注意到 Imperative-land 中的堆栈不断改变堆栈,而 Haskell 为每个堆栈操作创建一个新的si变量。然后他说,这些行中的每一条都可以被打成不同的顺序,结果不会改变。AFAICT 这与我sxOccurs的基本思想相同,当它不在乎我呈现子函数的顺序时。那么,这又是参照透明度的深层含义吗?如果没有,我在这里展示的是什么?

引用透明度意味着这个,而且只有这个:你可以用变量的定义替换变量,而不改变程序的含义。这称为"参照透明度",因为您可以"查看"对其定义的引用。

例如,您编写:

slOC Empty = 0
slOC (Scons se sls) = (seOC se) + (slOC sls)
seOC (AnAtom atm) = if (atm == oatm) then 1 else 0
seOC (AnSlist sla) = slOC sla

以下是您可以进行的一些转换,这要归功于引用透明度:

-- replace slOC by its definition
seOC (AnSlist sla) = (v -> case v of Empty -> 0; SCons se sls -> seOC se + slOC sls) sla
-- replace slOC by its definition *again*, starting from the previous line
seOC (AnSlist sla) = (v -> case v of
Empty -> 0
SCons se sls -> seOC se + (v -> case v of
Empty -> 0
SCons se sls -> seOC se + slOC sls
) sls
) sla
-- replace slOC by its definition in another equation
slOC (Scons se sls) = seOC se + (v -> case v of Empty -> 0; SCons se sls -> seOC se + slOC sls) sls
-- or, you could replace seOC by its definition instead
slOC (SCons se sls) = (v -> case v of
AnAtom atm -> if atm == oatm then 1 else 0
AnSlist sla -> sLOC sla
) se + slOC sls
-- or you could do both, of course

嗯,当然,对吧?到现在为止,你可能会想,"但是丹尼尔,这个属性怎么会失败?我将简要地转向另一种语言来说明:C。

int *x = malloc(sizeof(*x));
x[0] = 42;
printf("%dn", x[0]);

如果你没有很好地阅读 C,这会创建一个名为x的新变量,为其分配一些空间,将 42 写入该空间,然后打印出存储在该空间中的值。(我们可能应该期望它打印42!但我在第一行定义了x = malloc(sizeof(*x));我可以在其他地方用这个定义替换x吗?

不!这是一个非常不同的程序:

int *x = malloc(sizeof(*x));
malloc(sizeof(*x))[0] = 42;
printf("%dn", x[0]);

它仍然是一个语法上有效的程序,但现在x[0]在我们到达打印它的行的那一刻还没有初始化 - 因为我们分配了第二个独立的空间块,并初始化了另一个空间。

事实证明,这是其他语言违反引用透明度的主要方式:当您的变量的值可以更改时,用其定义的值替换对它们的引用是不安全的,要么是因为它从那时起可能已经更改,要么是因为这将导致它不会以程序其余部分预期的方式更改。哈斯克尔避开了这种能力;变量一旦赋值,就永远不会被修改。

正如注释中已经指出的那样,您描述的内容更准确地称为"相互递归",当两个函数在评估过程中相互调用时。实际上,引用透明度表示,给定完全相同的输入,函数将产生相同的输出。这在Python中是不正确的,我们可以编写这个函数。

global_var = 0
def my_function():
return global_var
my_function() # 0
global_var = 100
my_function() # 100

我们使用相同的输入调用my_function,但它神秘地产生了不同的输出。当然,在这个例子中,原因很明显,但引用透明度背后的想法是,在现实世界的代码中,它不会那么明显。如果你使用的语言没有引用透明度,而且如果该语言鼓励远程操作风格的突变,那么你将不可避免地得到访问你不知道的可变状态的函数。一个写得很好的函数将包含有关这些极端情况的大量文档,但如果你曾经使用过任何中型或更大的代码库,你就会知道"记录良好的函数"是一种罕见的景象。

在 Haskell 中,没有办法*编写像上面的 Python 函数这样的函数。在最坏的情况下,我们可以把它包裹在IO

myFunction :: IORef Int -> IO Int
myFunction = readIORef

但是现在,仅类型签名就一目了然地告诉我们,"这里发生了一些可疑的事情;买家要当心",即便如此,我们也只能访问IORef允许我们访问的一个全局变量。

*除了利用unsafePerformIO之外,没有办法在Haskell中编写函数,背后有很多龙。使用unsafePerformIO,我们可以非常明显地打破引用透明度,这就是为什么在一个名为"不安全"的模块中,每个 Haskell 教程都告诉您忘记和永远不要使用的功能。

最新更新