懒惰评估与急切评估相比有哪些优势?
有什么性能开销?懒惰评估会变得更慢还是更快?为什么(或者它取决于实施?)?
在大多数实现中,懒惰评估实际上是如何工作的?对我来说,它似乎会慢很多并且内存密集,因为变量必须存储操作以及数字。那么它如何在像Haskell这样的语言中工作(注意,我实际上并不知道那种语言)?如何实现和完成延迟,以便它不会非常慢/消耗更多空间?
答案 0 :(得分:13)
在创建具有有效摊销边界的数据结构时,延迟评估非常有用。
举一个例子,这里是一个不可变的堆栈类:
class Stack<T>
{
public static readonly Stack<T> Empty = new Stack<T>();
public T Head { get; private set; }
public Stack<T> Tail { get; private set; }
public bool IsEmpty { get; private set; }
private Stack(T head, Stack<T> tail)
{
this.Head = head;
this.Tail = tail;
this.IsEmpty = false;
}
private Stack()
{
this.Head = default(T);
this.Tail = null;
this.IsEmpty = true;
}
public Stack<T> AddFront(T value)
{
return new Stack<T>(value, this);
}
public Stack<T> AddRear(T value)
{
return this.IsEmpty
? new Stack<T>(value, this)
: new Stack<T>(this.Head, this.Tail.AddRear(value));
}
}
您可以在O(1)
时间内将项目添加到堆叠的前面,但需要O(n)
时间将项目添加到后方。由于我们必须重新构建整个数据结构,AddRear
是一个单片操作。
这是相同的不可变堆栈,但现在它被懒惰地评估:
class LazyStack<T>
{
public static readonly LazyStack<T> Empty = new LazyStack<T>();
readonly Lazy<LazyStack<T>> innerTail;
public T Head { get; private set; }
public LazyStack<T> Tail { get { return innerTail.Value; } }
public bool IsEmpty { get; private set; }
private LazyStack(T head, Lazy<LazyStack<T>> tail)
{
this.Head = head;
this.innerTail = tail;
this.IsEmpty = false;
}
private LazyStack()
{
this.Head = default(T);
this.innerTail = null;
this.IsEmpty = true;
}
public LazyStack<T> AddFront(T value)
{
return new LazyStack<T>(value, new Lazy<LazyStack<T>>(() => this, true));
}
public LazyStack<T> AddRear(T value)
{
return this.IsEmpty
? new LazyStack<T>(value, new Lazy<LazyStack<T>>(() => this, true))
: new LazyStack<T>(this.Head, new Lazy<LazyStack<T>>(() => this.Tail.AddRear(value), true));
}
}
现在,AddRear函数现在可以在O(1)
时间内运行。当我们访问Tail
属性时,它将评估一个Lazy值刚好以返回头节点,然后它停止,因此它不再是一个单片函数。
答案 1 :(得分:9)
懒惰评估是纯函数式编程语言的一个常见属性,可以“赢回性能”,它非常简单,只需在需要时评估表达式。例如,在Haskell中考虑
if x == 1 then x + 3 else x + 2
在严格(急切)评估中,如果x确实等于2,那么在Haskell中评估并返回x + 3,否则为x + 2,没有这样的事情发生,x + 3只是组成了表达式,例如,说我有:
let x = if x == 1 then x + 3 else x + 2
那么,我把它存储在一个变量中,但是如果我从来没有使用过那个变量因为其他一些条件呢?我在CPU上浪费了一个非常昂贵的整数。 (好吧,在实践中你没有赢得这个,但你得到更大的表达的想法)
那么问题就是,为什么所有语言都不是懒惰的?嗯,简单的原因是,在纯函数式语言中,表达式保证根本没有副作用。如果他们有,我们将不得不以正确的顺序评估它们。这就是为什么在大多数语言中他们都受到热切的评价。在表达式没有副作用的语言中,懒惰评估没有风险,所以它是一个合理的选择,可以赢回他们在其他区域中失去的性能。
另一个有趣的副作用是,Haskell中的if-then-else实际上是Bool -> a -> a -> a
类型的函数。在Haskell中,这意味着它接受一个类型为Boolean的参数,另一个类型为any,另一个类型与第一个相同,并再次返回该类型。您不会对不同的控制分支进行无限评估,因为只有在需要时才会对值进行求值,这通常是在编写了一个庞大的表达式时在程序的最后,然后对最终结果进行评估,丢弃编译器认为最终结果不需要的所有东西。例如,如果我将一个非常复杂的表达式单独划分,它可以在不评估两个部分的情况下替换为“1”。
差异在Scheme中可见,传统上严格评估,但是有一个名为Lazy Scheme的惰性变体,在Scheme (display (apply if (> x y) "x is larger than y" "x is not larger than y"))
中是一个错误,因为if
不是一个函数,它是一个专门的语法(虽然有人说语法在Scheme中并不特别),因为它不一定会评估它的所有参数,否则如果我们试图计算一个因子,我们就会耗尽内存。在Lazy Scheme中工作正常,因为在函数确实需要继续进行评估的结果之前,根本不会对这些参数进行评估,例如display。
答案 2 :(得分:7)
这是指对语法树的评估。如果您懒惰地评估语法树(即,当需要它表示的值时),您必须完整地执行计算的前面步骤。这是懒惰评估的开销。但是,有两个优点。 1)如果结果从未使用过,你将不会不必要地评估树; 2)你可以用某种递归形式表达和使用无限语法树,因为你只会将它评估到你需要的深度,而不是评估(急切地)全部,这是不可能的。
我不知道哈克尔,但据我所知,编程语言如python或ML急切地评估。例如,为了模拟ML中的延迟评估,您必须创建一个不带参数但返回结果的虚函数。这个函数是你的语法树,你可以随时通过使用空参数列表调用它来懒惰评估。
答案 3 :(得分:1)
在ruby中,我们使用函数修饰符(通常称为“once”)来包装方法,以便进行惰性求值。这样的方法只会被评估一次,值缓存,后续调用返回该值。
懒惰评估的一个用途(或误用)是使对象初始化的顺序是隐式的而不是显式的。之前:
def initialize
setup_logging # Must come before setup_database
setup_database # Must come before get_addresses
setup_address_correction # Must come before get_addresses
get_addresses
end
def setup_logging
@log = Log.new
end
def setup_database
@db = Db.new(@log)
end
def setup_address_correction
@address_correction = AddressCorrection.new
end
def get_addresses
@log.puts ("Querying addresses")
@addresses = @address_correction.correct(query_addresses(@db))
end
懒惰的评价:
def initialize
get_addresses
end
def log
Log.new
end
once :log
def db
Db.new(log)
end
once :db
def address_corrector
AddressCorrection.new
end
once :address_corrector
def get_addresses
log.puts ("Querying addresses")
@addresses = address_corrector.correct(query_addresses(db))
end
各种相互依赖的对象的初始化顺序现在是(1)隐式的,(2)是自动的。在缺点方面,如果过分依赖这种技巧,控制流程可能是不透明的。