1

我有这个代码

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

正如您在我的 The Little MLersxOccurs所说的那样,我在其中有两个帮助函数,let它们是“相互引用的” :和. 所以在 SML 中,你必须使用关键字让他们相互了解并相互“交叉引用”。顺便说一句,计算 s 列表中特定对象的数量,在我的示例中,原子是变量。slOCseOCandsxOccursAnAtomFruit

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

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它不在乎我呈现子功能的顺序。那么,这又是指代透明性的更深层含义吗?如果不是,我在这里展示的是什么?

4

2 回答 2

7

引用透明意味着这一点,并且仅意味着这一点:您可以用变量的定义替换变量,而不会改变程序的含义。这被称为“引用透明度”,因为您可以“查看”对其定义的引用。

例如,你写:

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 避开了这种能力。变量一旦被赋值,就永远不会被修改。

于 2021-03-21T03:58:52.443 回答
3

正如评论中已经指出的那样,您所描述的更准确地称为“相互递归”,当两个函数在评估过程中相互调用时。实际上,参照透明性表明,给定完全相同的输入,一个函数将产生相同的输出。这在 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让我们可以访问。

* Haskell中没有写函数的方法,除了exploiting unsafePerformIO,背后有很多龙。有了unsafePerformIO,我们可以很明显地打破引用透明性,这就是为什么它是一个名为“不安全”的模块中的一个名为“不安全”的函数,每个 Haskell 教程都告诉你忘记并且永远不要使用它。

于 2021-03-21T04:05:04.767 回答