Haskell 哈斯克尔的回忆录

Haskell 哈斯克尔的回忆录,haskell,functional-programming,lazy-evaluation,memoization,pure-function,Haskell,Functional Programming,Lazy Evaluation,Memoization,Pure Function,: 可以通过备忘录加快速度: fib_memo = {} def fib(n): if n < 2: return 1 if not fib_memo.has_key(n): fib_memo[n] = fib(n-1) + fib(n-2) return fib_memo[n] 这种记忆化的实现技术在许多领域得到了广泛的应用 编程语言,但不能直接应用于Haskell 因为Haskell是纯的,我们不想引入杂质 记忆一个函数。幸运的是,记忆一个 由

:

可以通过备忘录加快速度:

fib_memo = {}
def fib(n):
    if n < 2: return 1
    if not fib_memo.has_key(n):
        fib_memo[n] = fib(n-1) + fib(n-2)
    return fib_memo[n]
这种记忆化的实现技术在许多领域得到了广泛的应用 编程语言,但不能直接应用于Haskell 因为Haskell是纯的,我们不想引入杂质 记忆一个函数。幸运的是,记忆一个 由于Haskell的懒惰本性,功能无副作用 评估

下面的memoize函数采用Int->a类型的函数 并返回同一函数的记忆版本。诀窍在于 将函数转换为值,因为在Haskell中,函数不是 记忆化,但价值观是

问题:

函数编程中的函数和值不一样吗? 在纯函数的上下文中,缓存如何不被视为副作用
所有函数都是值,但并非所有值都是函数

这实际上是关于操作语义的,在Haskell中有时很难讨论,因为Haskell只是根据其指称语义定义的——也就是说,表达式的求值是什么值,而不是如何求值。这并不是一个副作用,因为记忆化的状态本质仍然隐藏在纯度的抽象背后:虽然在程序的局部图简化中表示了一些内部状态,但您的程序无法以将其与非记忆化版本区分开来的方式观察该状态。这里的一个微妙之处是,这些记忆化策略实际上并不需要进行记忆化——可以保证的是,它们会在一段不确定的有限时间后给出结果

Haskell实现不需要记忆任何内容——例如,它可以使用纯名称调用,它不记忆值,而是重新计算所有内容。下面是一个按姓名呼叫的示例

let f x = x * x in f (2 + 2)
= (2 + 2) * (2 + 2)
= 4 * (2 + 2)
= 4 * 4
= 16
这里2+2计算两次。大多数Haskell实现的优化都会创建一个thunk,这样最多只能计算一次,这就是所谓的按需调用。但是,一个名为Haskell的实现对其进行了两次评估,在技术上是符合要求的。因为Haskell是纯的,所以计算的结果没有差别。然而,在现实世界中,这种策略的成本太高而不实用

至于不记忆函数的选择,逻辑是相同的。对于一个编译器来说,使用类似的方法积极地记忆每个函数是完全符合要求的。Haskell的纯度意味着,如果选择这种评估策略,结果不会有任何差异。但是,在现实世界的应用程序中,像这样记忆每个函数最终都会占用大量内存,而最佳计算器的开销太高,无法提供良好的实际性能

Haskell编译器也可以选择记忆某些函数,但不记忆其他函数,以最大限度地提高性能。这将是伟大的,我的理解是,它不是真的知道如何可靠地做到这一点。编译器很难预先判断哪些计算是便宜的,哪些是昂贵的,哪些计算可能会被重用,哪些不会


因此选择了一种平衡,在这种平衡中,值的计算形式通常小于它们的thunk,并被记忆;而函数,因为它们需要一个完整的备忘表,所以它们的备忘形式通常大于它们的定义,而不是。然后,根据我们作为程序员的判断,我们得到了一些类似于本文中的技术,可以在这些表示之间来回切换。

所有函数都是值,但并非所有值都是函数

这实际上是关于操作语义的,在Haskell中有时很难讨论,因为Haskell只是根据其指称语义定义的——也就是说,表达式的求值是什么值,而不是如何求值。这并不是一个副作用,因为记忆化的状态本质仍然隐藏在纯度的抽象背后:虽然在程序的局部图简化中表示了一些内部状态,但您的程序无法以将其与非记忆化版本区分开来的方式观察该状态。这里的一个微妙之处是,这些记忆化策略实际上并不需要进行记忆化——可以保证的是,它们会在一段不确定的有限时间后给出结果

Haskell实现不需要记忆任何内容——例如,它可以使用纯名称调用,它不记忆值,而是重新计算所有内容。下面是一个按姓名呼叫的示例

let f x = x * x in f (2 + 2)
= (2 + 2) * (2 + 2)
= 4 * (2 + 2)
= 4 * 4
= 16
这里2+2计算两次。大多数Haskell实现的优化都会创建一个thunk,这样最多只能计算一次,这就是所谓的按需调用。但是,一个名为Haskell的实现对其进行了两次评估,在技术上是符合要求的。贝考 se Haskell是纯的,计算结果不会有差异。然而,在现实世界中,这种策略的成本太高而不实用

至于不记忆函数的选择,逻辑是相同的。对于一个编译器来说,使用类似的方法积极地记忆每个函数是完全符合要求的。Haskell的纯度意味着,如果选择这种评估策略,结果不会有任何差异。但是,在现实世界的应用程序中,像这样记忆每个函数最终都会占用大量内存,而最佳计算器的开销太高,无法提供良好的实际性能

Haskell编译器也可以选择记忆某些函数,但不记忆其他函数,以最大限度地提高性能。这将是伟大的,我的理解是,它不是真的知道如何可靠地做到这一点。编译器很难预先判断哪些计算是便宜的,哪些是昂贵的,哪些计算可能会被重用,哪些不会


因此选择了一种平衡,在这种平衡中,值的计算形式通常小于它们的thunk,并被记忆;而函数,因为它们需要一个完整的备忘表,所以它们的备忘形式通常大于它们的定义,而不是。然后,根据我们作为程序员的判断,我们获得了一些类似于本文中的技术,可以在这些表示之间来回切换。

您有时可以通过使用corecursion自引用数据结构免费进行记忆:fibs=1:1:zipWith+fibs tail fibs。我发现该页面有点混乱,老实说它没有显示基本形式的备忘录,而是立即转到fibMemo=fixmemoize。fib相当微妙,取决于fix的定义。在我看来,理解备忘录的第一步是理解在没有优化的情况下,f=\x->let y=x+y中的昂贵42和f=let y=x+y中的昂贵42在操作上是如何不同的。前者在每次调用时计算42次,后者只计算一次。副作用只指函数做了什么,而不是它做得有多快或慢。其主要作用是计算和产生回报值;它的副作用是它所引起的外部世界状态的任何变化。该函数本身被视为一个黑匣子。有时,通过使用corecursion自引用数据结构,您可以免费记忆:fibs=1:1:zipWith+fibs tail fibs。老实说,我发现该页面有点混乱。它没有显示基本形式的备忘录,而是立即转到fibMemo=fixmemoize。fib相当微妙,取决于fix的定义。在我看来,理解备忘录的第一步是理解在没有优化的情况下,f=\x->let y=x+y中的昂贵42和f=let y=x+y中的昂贵42在操作上是如何不同的。前者在每次调用时计算42次,后者只计算一次。副作用只指函数做了什么,而不是它做得有多快或慢。其主要作用是计算和产生回报值;它的副作用是它所引起的外部世界状态的任何变化。函数本身被视为一个黑匣子。