【问题标题】:Lazy vs eager evaluation and double linked list building懒惰与急切的评估和双链表构建
【发布时间】:2012-03-05 20:06:45
【问题描述】:

我睡不着! :)

我在 Haskell 中编写了构建双链表的小程序。使其成为惰性评估的基本语言的属性(请参见下面的一堆代码)。我的问题是,我可以用 eager 评估的 pure 功能语言做同样的事情吗?无论如何,eager 函数式语言必须具备哪些属性才能构建这样的结构(杂质?)?

import Data.List

data DLList a = DLNull |
    DLNode { prev :: DLList a
           , x :: a
           , next :: DLList a
           }
  deriving (Show)

walkDLList :: (DLList a -> DLList a) -> DLList a -> [a]
walkDLList _ DLNull = []
walkDLList f n@(DLNode _ x _)  = x : walkDLList f (f n)

-- Returns first and last items.
makeDLList :: [a] -> (DLList a, DLList a)
makeDLList xs = let (first, last) = step DLNull xs in (first, last)
  where
    step prev [] = (DLNull, prev)
                         -- Here I use laziness. 'next' is not built yet, it's a thunk.
    step prev (x : xs) = let this = DLNode prev x next 
                             (next, last) = step this xs
                         in (this, last)

testList :: [Int] -> IO ()
testList l = let
    (first, last) = makeDLList l
    byNext = walkDLList next first
    byPrev = walkDLList prev last
  in do
    putStrLn $ "Testing: " ++ show l
    print byNext
    print byPrev

main = do
  testList []
  testList [1, 2, 3, 4]

【问题讨论】:

  • 查看this code。 :) 它只返回头节点,而且非常简单。

标签: list haskell lazy-evaluation eager


【解决方案1】:

双向链表可以在热切的语言中以纯粹的功能方式实现为单链表上的zipper。例如,请参阅Rosetta Code > Doubly-linked list > OCaml > Functional

【讨论】:

  • 我认为双重链接列表的意义在于它确实在遍历时分配新的存储空间,这与您链接到的代码不同,Dan .在其中,prev(next alist)alist 不同的存储空间,不像通常的 C,比如实现,以及上面问题中的 Haskell 代码,在 let (alist,zlist)=makeDLList xs 之后我们有 prev(next alist)==alistnext(prev zlist)==zlist,其中 @ 987654328@ 表示“相同的实际存储”,或者用 Lisp 的说法eq-equivalent”(当然是非空的xs)。跨度>
  • @WillNess 关于“在遍历时不分配新存储”,这是一个实现细节,需要付出代价。您不能在不复制整个列表的情况下“插入”到不可变的双向链表(以 OP 给出的样式)。好吧,由于懒惰,您通常可以减少复制,但关键是新的双向链表不能与旧的双向链表共享任何节点。不可变 zipper 实现允许更多共享,但代价是遍历需要更多存储空间(但不是太多,同样由于共享能力)。
  • @WillNess n.b. Haskell 对eq-ness 不做任何保证。 GHC 的并行垃圾收集器利用了这一点,并且实际上可以删除您可能期望存在的共享。这种情况很少见,几乎不会影响性能,而且由于数据不可变,它永远不会影响您的结果。
  • 我希望如果相同的名称被使用两次,(指向)相同的值将在每个地方使用:在let {this = DLNode prev x next ; (next, last) = step this xs}thisthis,正确的?不就是懒惰的意思吗?一个名字 - 一个(指向一个)实体,不是吗?您对插入的整个列表复制是完全正确的。也许 zipper 在某些情况下更好,而 DL-list 在其他情况下更好。他们仍然不同。
  • 另外,如果 GHC 会从同一变量的两次使用中删除共享,它将如何影响依赖它的各种核心递归定义,例如fibs = 0:scanl (+) 1 fibs?它甚至会起作用,还是会大大降低效率?
【解决方案2】:

只要一种语言具有闭包、lambda 等功能,您就可以随时模拟惰性。您甚至可以在 Java 中重写该代码(无需改变变量等),您只需将每个“惰性”操作包装在类似

interface Thunk<A> {
   A eval();  
}

当然这看起来很糟糕,但这是可能的。

【讨论】:

  • 你需要一些可更新的东西来获得惰性评估的效果(最多评估一次),否则你将模拟按名称调用。
  • 那么,augustus,你的意思是仅仅闭包和 lambdas 是不够的?
  • 我认为他只是指eval 的结果需要缓存,所以如果eval 多次运行,则不会重新计算thunk。
【解决方案3】:

在 Prolog 的非回溯子集中,可以看作是 explicitly set-once 渴望的纯函数式语言,您可以轻松地构建双向链表。正是引用透明性让 Haskell 变得困难,因为它禁止 Prolog 显式设置 named、显式 not-yet-set 逻辑变量,而是强制 Haskell以“打结”的扭曲方式达到同样的效果。我想。

另外,Haskell 的 惰性求值下的保护递归 与 Prolog 的以尾递归取模 方式构建的开放式列表之间并没有太大区别。国际海事组织。例如,这是lazy lists in Prolog 的示例。 memoized共享存储用作通用访问中介,因此可以将之前的计算结果安排缓存。

想一想,如果您在设置任何变量或任何指针后从不重置任何变量或任何指针,那么您可以以一种限制性的方式将 C 用作一种急切的纯函数式语言。你仍然有空指针,就像 Prolog 有变量一样,显式设置一次也是如此。当然,您可以使用它构建双向链表。

所以剩下的唯一问题是,您是否承认像 pure 这样的 set-once 语言?

【讨论】:

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