【问题标题】:Non-Trivial Lazy Evaluation非平凡的懒惰评估
【发布时间】:2011-10-23 19:16:56
【问题描述】:

我目前正在消化 Keegan McAllister 的精彩演示为什么要学习 Haskell?。在那里他使用了sn-p

minimum = head . sort

通过说明 minimum 在 Haskell 中具有 时间复杂度 O(n) 来说明 Haskell 的惰性求值。但是,我认为这个例子本质上是一种学术性的。因此,我要求提供一个更实际的示例,其中大部分中间计算都被丢弃了。

【问题讨论】:

  • 这个例子是学术性的,而惰性评估的好处是不明显的。为什么要后者?看到实际代码可以从惰性求值中受益还不够吗?
  • “计算机科学中的所有问题都可以通过另一个层次的间接性来解决”。懒惰也是一种间接性。

标签: haskell time-complexity lazy-evaluation


【解决方案1】:
  • 你写过人工智能吗?您必须通过树遍历函数对修剪信息(例如最大深度、相邻分支的最小成本或其他此类信息)进行线程化,这不是很烦人吗?这意味着每次想要改进 AI 时都必须编写新的树遍历。那是愚蠢的。使用惰性求值,这不再是问题:编写一次树遍历函数,生成一个巨大的(甚至是无限的!)游戏树,并让您的消费者决定消耗多少。

  • 编写一个显示大量信息的 GUI?想要它跑得快吗?在其他语言中,您可能必须编写仅呈现可见场景的代码。在 Haskell 中,您可以编写渲染整个场景的代码,然后选择要观察的像素。同样,渲染一个复杂的场景?为什么不计算各种细节级别的无限场景序列,并在程序运行时选择最合适的场景?

  • 您编写了一个昂贵的函数,并决定记忆它以提高速度。在其他语言中,这需要构建一个数据结构来跟踪您知道答案的函数的哪些输入,并在您看到新输入时更新结构。记住让它线程安全——如果我们真的需要速度,我们也需要并行性!在 Haskell 中,您构建了一个无限数据结构,每个可能的输入都有一个条目,并评估与您关心的输入相对应的数据结构部分。线程安全是免费的。

  • 这是一个可能比以前的更平淡无奇的一个。您是否曾经发现 && 和 || 不是您唯一想要短路的东西?我肯定有!例如,我喜欢 <|> 函数用于组合 Maybe 值:它采用第一个实际具有值的参数。所以Just 3 <|> Nothing = Just 3; Nothing <|> Just 7 = Just 7;和Nothing <|> Nothing = Nothing。此外,它是短路的:如果它的第一个参数是Just,它就不会费心进行计算来确定它的第二个参数是什么。

    并且<|> 没有内置在语言中;它由图书馆补充。那就是:懒惰让你写出全新的短路形式。 (事实上​​,在 Haskell 中,即使是 (&&) 和 (||) 的短路行为也不是内置的编译器魔法:它们自然产生于语言的语义以及它们在标准库中的定义。)

一般来说,这里的共同主题是,您可以将值的产生与确定哪些值有趣分开。这使事情变得更加可组合,因为制作人不需要知道选择哪些有趣的东西。

【讨论】:

  • 哇,令人信服的论据——恕我直言,Haskell 的未来充满希望。所以<|> 操作符等价于(Emacs-)Lisp 的or 对可能总是nil 的对象的行为,对吧?无论如何,我真的很怀念其他语言中的这种行为。
  • thread pruning information 是什么意思?它是某种特定树的分支性能统计信息的簿记吗?
  • @Nordlöw 是的,完全正确。例如,当你第一次实现一个 AI 并且只是想搞出一些东西时,你可能会决定做一个蛮力的 n-ply 搜索。但是你编写的 n 层搜索函数必须完成两个任务:一个是搜索游戏树,另一个是跟踪你已经完成了多少层。后来,当您决定使用 alpha-beta 剪枝而不是蛮力时,由于关注点交织在一个函数中,您的手会有点束缚。惰性允许您将搜索博弈树和修剪搜索树的关注点分开。
  • 当然,只有当存储所有可能的结果确实可行时,记忆才会变得更简单。如果你只能记住,例如,n 个评估最多的结果(但在编译时不知道这些结果会是哪一个),Haskell 的纯粹性实际上使它更难做到。
【解决方案2】:

这是我昨天发布到另一个线程的一个众所周知的例子。汉明数是没有任何大于 5 的素因数的数字。即它们的形式为 2^i*3^j*5^k。其中前 20 个是:

[1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,30,32,36]

第500000个是:

1962938367679548095642112423564462631020433036610484123229980468750

打印第 500000 个的程序(经过短暂的计算)是:

merge xxs@(x:xs) yys@(y:ys) =
  case (x`compare`y) of
    LT -> x:merge xs yys
    EQ -> x:merge xs ys
    GT -> y:merge xxs ys

hamming = 1 : m 2 `merge` m 3 `merge` m 5
  where
    m k = map (k *) hamming

main = print (hamming !! 499999)

用非惰性语言以合理的速度计算该数字需要更多的代码和令人头疼的问题。有很多examples here

【讨论】:

    【解决方案3】:

    考虑生成和使用 无限 序列的第一个 n 元素。如果没有惰性求值,天真的编码将在生成步骤中永远运行,并且永远不会消耗任何东西。使用惰性求值时,只会生成与代码尝试使用的元素一样多的元素。

    【讨论】:

      猜你喜欢
      • 2019-01-07
      • 1970-01-01
      • 1970-01-01
      • 2015-09-06
      • 1970-01-01
      • 2016-12-14
      • 2021-12-25
      • 2018-04-01
      • 2018-03-31
      相关资源
      最近更新 更多