无国籍意味着参照透明?

问题描述

我有这个代码

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 中有两个辅助函数,它们是“相互自我参照的”,正如我的 The Little 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

注意到命令式中的堆栈不断地改变堆栈,而 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("%d\n",x[0]);

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

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

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

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

事实证明这是其他语言违反引用透明性的主要方式:当变量的值可以改变时,用它们定义的值替换对它们的引用是不安全的,要么因为它可能从那时起就发生了变化,或者因为这将导致它不会像程序的其余部分期望的那样改变。 Haskell 避开了这种能力;变量一旦赋值,就永远不会被修改。

,

正如评论中已经指出的那样,当两个函数在求值过程中相互调用时,您所描述的更准确地称为“相互递归”。引用透明实际上表示,给定完全相同的输入,函数将产生相同的输出。这在 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,我们可以很明显地打破引用透明性,这就是为什么它是一个名为“unsafe”的模块中的一个名为“unsafe”的函数,每个 Haskell 教程都告诉你忘记并且永远不要使用。