【问题标题】:Statelessness implies referential transparency?无状态意味着参照透明?
【发布时间】:2021-06-18 00:29:38
【问题描述】:

我有这个代码

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

【问题讨论】:

  • “参照透明”的意思是“相同的输入产生相同的输出”。
  • 那我上面说了什么?
  • 第一个例子是“相互递归”,第二个我不确定它有没有专门的术语。

标签: haskell stateless referential-transparency


【解决方案1】:

正如 cmets 中已经指出的那样,您所描述的更准确地称为“相互递归”,即两个函数在评估过程中相互调用。实际上,参照透明性表明,给定完全相同的输入,一个函数将产生相同的输出。这在 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 教程都告诉你忘记并且永远不要使用它。

【讨论】:

    【解决方案2】:

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

    例如,你写:

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

    【讨论】:

      猜你喜欢
      • 2017-09-01
      • 2017-06-22
      • 1970-01-01
      • 2012-10-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-11
      • 2020-04-28
      相关资源
      最近更新 更多