了解递归惰性列表

时间:2015-12-29 15:55:42

标签: haskell

我正在尝试使用Haskell wiki

中的以下代码段
primesPE = 2 : oddprimes
  where 
    oddprimes = sieve [3,5..] 9 oddprimes
    sieve (x:xs) q ps@ ~(p:t)
      | x < q     = x : sieve xs q ps
      | otherwise =     sieve (xs `minus` [q, q+2*p..]) (head t^2) t

minus (x:xs) (y:ys) = case (compare x y) of
  LT -> x : minus xs (y:ys)
  EQ -> minus xs ys
  GT -> minus (x:xs) ys
minus xs _ = xs

我正在通过递归执行此操作的函数来查看oddprimes的递归定义和无限列表上minus的使用。

我认为部分我感到困惑,因为我不明白Haskell编译器如何执行此代码。怎么没有内存耗尽?我怀疑答案是<magic> lazy evaluation </magic>,但我认为我需要更加牢固地掌握如何在实践中评估这种感觉是否舒适。

2 个答案:

答案 0 :(得分:2)

在幕后,Haskell系统通常通过一种称为 thunk 的结构来表示内存中的值。将thunk视为具有两种状态的对象:

  1. Uncomputed
  2. 已计算
  3. 未计算的thunk包含指向计算其结果的目标代码子例程的指针,以及指向提供执行该计算所需的值的thunk的指针。 (如果你遇到closures的概念,那么未计算的thunk就是一个闭包。)

    计算的thunk只包含原始结果值。 thunks的基本操作称为强制。当您强制执行未计算的thunk时,将使用捕获的参数调用其子例程,将thunk替换为计算结果值(从而将其状态切换为计算值),并返回该值。当你强制已经计算好的thunk时,你只需得到已计算的值。

    如果我们用伪代码编写,这可能会更容易。我会做一些Java-ish:

    class Thunk<A> {
        private final Supplier<A> computation;
        private boolean computed = false;
        private A result;
    
        Thunk(Supplier<A> computation) {
            this.computation = computation;
        }
    
        public A force() {
            if (!computed) {
                result = computation.get();
                computed = true;
            }
            return result;
        }
    }
    

    这与我上面描述的不完全相同,但确实表现得像它。

    现在,让我们看一个更简单的例子,一个构造一个元素的重复列表的函数:

    repeat :: a -> [a]
    repeat a = a : repeat a
    

    repeat函数被编译为一个目标代码例程,在伪代码中可能看起来像这样:

    Thunk<List<A>> repeat(Thunk<A> a) {
        return new Thunk<A>(() -> new Cons<A>(a, repeat(a)));
    }
    
    class Cons<A> extends List<A> {
        Thunk<A> head;
        Thunk<List<A>> tail;
        // ...
    }
    

    如果您不熟悉Java 8,() -> new Cons<A>(a, repeat(a))是一个lambda。这个函数接受零参数,并在调用时构造一对。对repeat的递归调用是lambda 中的,因此调用repeat不会递归 - 它会返回捕获lambda的Thunk,而不会正确执行远。当那个thunk为force d时,只有然后将调用lambda,它将调用repeat,它将立即返回另一个类似的thunk。

    基本上,在Haskell中,代码被编译为优化的低级版本。

答案 1 :(得分:0)

答案确实只是懒惰的评价。 Haskell不评估任何内容,甚至不评估列表的尾部,除非它被明确标记为严格,或者IO动作需要它(甚至IO动作本身可能是懒惰的,有时候是混乱)

所以,如果你评估像

这样的东西
head (1 : undefined)

你会得到1,即使那里有未定义的,因为列表的尾部永远不会被评估。