在函数式编程语言的实现中,“sharing”是指什么?

5

共享是指如果数据将被多次使用,则会存储临时数据。也就是说,函数只评估其参数一次。一个例子是:

let x = sin x in x * x

其他哪些特性有助于“共享”,它们将如何与需要执行IO的实际程序进行交互?


3
将数据缓存以供重用的行为被称为“记忆化(memoization)”,而不是“共享(sharing)”。如果您有包含相同子树的树形数据,则将其转换为有向无环图(DAG)以“共享”共同的子树;这就是我所谓的共享。此外,由于问题的开放性质,它并不完全符合SO的格式。您能否更具体地说明,并给出示例... - chris
3个回答

7

分享是一种平等,即指针平等。在 Haskell 的值领域(语义解释)中,没有这样的分享。只有当它们具有 Eq 的实例并且“相等性”被定义为二元关系 (==) 时,值才能相等。

因此,分享通过引用这种基础的指针平等来逃避语义解释,由于实现而存在,而不是语义。但有时这是一个有用的副作用。不幸的是,由于 Haskell 是由其语义解释来定义的,使用共享是未定义的行为。它与特定的实现有关。一些库使用共享,因此与 GHC 相关联。

然而,有一种内置的共享机制。这是通过 StableName 接口公开的。我们使用 makeStableName :: a -> IO (StableName a) 生成 StableName a 对象,并有一个 instance Eq (StableName a)——因此 StableName任何 类型引入了某种形式的相等性。

StableName的相等性几乎等同于指针相等性。引用Haddock文档的话来说,如果sn1 :: StableNamesn2 :: StableName以及sn1 == sn2,那么sn1sn2是通过对同一对象调用makeStableName而创建的。请注意,这只是一个“if”语句,而不是“if and only if”。有时,两个东西可以是“指针等效”的,但仍然具有不同的稳定名称,这是由Haskell留给GC的灵活性所迫使的,并且是一个漏洞,即使在实现中根本没有“指针相等性”,StableName也可以存在于任何Haskell实现中。这些StableName仍然没有语义意义,但因为它们是在“IO”单子中引入的,所以“没问题”。相反,它们可能被认为是在任何类型上实现最小(最具体)等式关系的(具有讽刺意味的)不稳定子集。

5

在函数式编程中,共享的最明显的例子来自于基于图重写的Clean语言。在那里,一项计算涉及到DAG,因此我们可以将表达式(sin x) * (sin x)视为

    (*)
  /     \
sin x   sin x

图重写系统具有明确的共享概念,因此我们可以将计算表示为:
   (*)
   / \
   \ /
  sin x

将乘法节点指向同一节点,从而共享对于 "sin x" 的计算。术语重写系统没有如此明确的共享概念,但优化仍然是相关的。在 GHC 中,有时可以使用局部变量来表示共享,例如重写。
f x = (sin x) * (sin x)

转换为

f x = sinx * sinx
  where sinx = sin x

但由于这两者在语义上是等价的,编译器可以自由地以相同的方式实现它们,无论是共享还是不共享。据我所知,GHC通常会保留引入局部变量的共享,并有时引入它(将共享添加到第一个),但在术语重写系统中没有形式化的共享表达式,因此这两种行为都依赖于具体实现(请参见tel的评论和答案)。

共享涉及IO,因为有副作用的值不能被共享。如果我们考虑一个不纯的语言,那么存在以下区别:

(string-append (read-line)
               (read-line))

并且

(let ((s (read-line)))
  (string-append s s))

第一个执行IO操作两次,从用户请求两行数据并附加它们; 第二个“分享”IO操作,执行一次并将其附加到自身。通常,共享纯计算可以减少执行时间而不改变程序的结果,而共享影响值(可能随时间变化或与用户交互的值)会更改结果。对于编译器自动共享计算,它需要知道它们是纯的,因此减少评估次数不重要。Clean使用独特类型来实现此目的; IO操作具有属性UNQ的类型,告诉编译器它不应该被共享。 Haskell以IO单子略微不同的方式执行同样的操作。

实际上,你的 where sinx = sin x 的例子没有必要引入共享。纯度保证了无论被乘的两个值是否“指针相等”,表达式的最终值都是相同的。现在,共享这个值显然是一种优化,GHC有时会“向上浮动”来引入这种共享。话虽如此,如果我们想要的话,我们也可以计算 sin x,将其复制到两个地址,然后再进行乘法运算。在Haskell的语义中,共享是不可观察的StableName 是你能够接近而不涉及实现细节的最接近方法。 - J. Abrahamson
真的(现已编辑)。我发现一般这样的where从句会在GHC中引入共享,但透明地共享值的反面是透明地取消共享(内联 sin x)的能力。 - isturdy
是的,同意,GHC 往往能够很好地发现共享机会。letwhere 似乎可以保证它。 - J. Abrahamson

1

你的例子并不是共享的例子--它只是将一个值乘以自身(然后丢弃原始值)。

共享是指一个数据结构的某部分在较大的数据结构中出现两次,或者在不同的数据结构中出现两次。例如:

p  = (1, 2)
pp = (p, p)  -- p is shared between the first and second component of pp

xs = [1, 2, 3]
ys = 0::1::xs
zs = 5::xs  -- ys and zs share the same tail

在内存中,这样的共享将导致DAG结构。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接