Haskell:非严格和懒惰有何区别?


68

我经常读到懒惰非严格不一样,但是我很难理解它们之间的区别。它们似乎可以互换使用,但我知道它们具有不同的含义。我希望能帮助您理解其中的区别。

我对此帖子有一些疑问。我将在本文结尾总结这些问题。我有一些示例代码片段,我没有对其进行测试,仅将它们作为概念进行了介绍。我添加了报价,以免您查找报价。也许它将对以后的某个问题有所帮助。

非严格Def:

如果函数f在应用于非终止表达式时也未能终止,则称其为严格函数。换句话说,当f bot的值是|时,f是严格的。对于大多数编程语言,所有功能都是严格的。但这在Haskell中并非如此。举一个简单的例子,考虑const1,常量1函数,其定义为:

const1 x = 1

在Haskell中const1 bot的值是1。从操作上来说,由于const1不需要参数的值,因此它从不尝试对其求值,因此也不会陷入无限终止的计算中。因此,非严格函数也称为“惰性函数”,并说是“懒惰地”或“根据需要”评估其参数。

-一个温柔的介绍哈斯克尔:函数

我真的很喜欢这个定义。这似乎是我了解严格的最佳选择。是const1 x = 1懒惰的呢?

非严格意味着减少(评估的数学术语)从外部进入,

因此,如果您有(a +(b c)),则首先减小+,然后减小内部(b c)。

-哈斯克尔百科:懒惰与非严格

Haskell Wiki确实使我感到困惑。我了解他们在说什么订单,但是我看不到(a+(b*c))如果通过了,将如何严格地评估_|_

在非严格评估中,不评估函数的参数,除非在函数主体的评估中实际使用了参数。

在教会编码下,运算符的惰性评估映射为功能的非严格评估;因此,非严格评估通常称为“惰性”。许多语言中的布尔表达式使用一种称为短路评估的非严格评估形式,在这种评估中,只要可以确定会产生明确的布尔值,评估就会立即返回,例如,在遇到真值的析取表达式中,或者遇到假的合取表达式,依此类推。条件表达式通常也使用惰性求值,即在得到明确的分支后立即返回求值。

-维基百科:评估策略

懒惰的Def:

另一方面,惰性求值意味着仅在需要表达式的结果时才对表达式求值(请注意从“归约”到“求值”的转变)。因此,当评估引擎看到一个表达式时,它将构建一个thunk数据结构,其中包含评估该表达式所需的任何值以及指向该表达式本身的指针。当实际需要结果时,评估引擎将调用该表达式,然后将结果替换为thunk以供将来参考。...

显然,一个笨拙的表情和一个部分评估的表情之间有很强的对应关系。因此,在大多数情况下,术语“惰性”和“非严格”是同义词。但不完全是。

-哈斯克尔百科:懒惰与非严格

这似乎是Haskell的特定答案。我认为懒惰意味着沉重,非严格意味着部分评估。比较是否简化了?不总是意味着的thunk和非严格总是意味着部分评价。

在编程语言理论中,惰性评估或按需调用1是一种评估策略,它会延迟对表达式的评估,直到实际需要其值为止(非严格评估),并且还避免了重复评估(共享)。

-维基百科:懒惰的评估

势在必行的例子

我知道大多数人在学习功能语言时都会说忘记命令式编程。但是,我想知道这些是否属于非严格,懒惰,或者两者兼有?至少它会提供一些熟悉的东西。

短路

f1() || f2()

C#,Python和其他具有“ yield”功能的语言

public static IEnumerable Power(int number, int exponent)
{
    int counter = 0;
    int result = 1;
    while (counter++ < exponent)
    {
        result = result * number;
        yield return result;
    }
}

- MSDN:收率(C#)

回呼

int f1() { return 1;}
int f2() { return 2;}

int lazy(int (*cb1)(), int (*cb2)() , int x) {
    if (x == 0)
        return cb1();
    else
        return cb2();
}

int eager(int e1, int e2, int x) {
    if (x == 0)
         return e1;
    else
         return e2;
}

lazy(f1, f2, x);
eager(f1(), f2(), x);

问题

我知道所有这些资源的答案就在我眼前,但我无法把握。似乎该定义很容易被隐含或显而易见地忽略。

我知道我有很多问题。随时回答您认为相关的任何问题。我添加了这些问题进行讨论。

  • const1 x = 1也懒?
  • 如何从“内向”非严格评估?是因为向内允许减少不必要的表达式,例如in const1 x = 1?减少似乎符合懒惰的定义。
  • 总是意味着的thunk非严格总是意味着局部的评价?这只是一个概括吗?
  • 以下命令性概念是懒惰的,非严格的,两者都是还是都不是?
    • 短路
    • 使用产量
    • 传递回调以延迟或避免执行
  • 懒惰的一个子集不严格或反之亦然,或者是它们相互排斥。例如是有可能是不严格而不懒惰,或懒惰而不非严格
  • Haskell的非严格性是通过懒惰实现的吗?

谢谢你这么!

Answers:


60

非严格和懒惰,尽管可以非正式地互换,但适用于不同的讨论领域。

非严格语义:表达的数学含义。不受严格限制的世界没有功能运行时间,内存消耗甚至计算机的概念。它仅讨论域中的哪些类型的值映射到共域中的哪些类型的值。特别是,严格函数必须将值⊥(“底部”-有关更多信息,请参见上面的语义链接)映射为⊥;非严格功能不允许这样做。

懒惰是指操作行为:在真实计算机上执行代码的方式。大多数程序员在操作上都考虑程序,因此这可能就是您所想的。惰性评估是指使用thunk的实现-指向代码的指针,这些指针在首次执行时将被替换为一个值。注意此处的非语义词:“指针”,“第一次”,“已执行”。

惰性评估引起非严格的语义,这就是为什么这些概念看起来如此紧密的原因。但是正如FUZxxl所指出的,懒惰并不是实现非严格语义的唯一方法。

如果您有兴趣了解有关此区别的更多信息,我强烈建议您使用上面的链接。阅读它是我对计算机程序含义的理解的转折点。


11
如果您喜欢视频,这些讲座是一个隐藏的宝石。后面的视频花费大量时间讨论Haskell的指称语义。不用担心,尽管页面是德语的,但是讲座都是用英语进行的。
hammar 2011年

@hammar谢谢你!
jub0bs 2015年

18

评估模型的一个例子,既不严格也不懒惰: 乐观评估,由于可以避免很多“轻松”的重击,因此可以加快速度:

乐观评估意味着,即使可能不需要子表达式来评估超表达,我们仍然会使用一些启发式方法来评估其中的一些。如果子表达式不能足够快地终止,我们将暂停它的评估,直到真正需要它为止。如果以后需要子表达式,则相对于惰性评估,我们有一个优势,因为我们不需要生成转换。另一方面,如果表达式不终止,我们也不会损失太多,因为我们可以足够快地终止它。

如您所见,此评估模型并不严格:如果评估但不要求产生_ | _的结果,则该函数仍将终止,因为引擎将中止评估。另一方面,可能会计算出比所需更多的表达式,因此并不完全是懒惰的


好一点,尽管我会用[sic]乱扔那句话或简单地对其进行编辑。
乔恩·普迪

@FUXxxl:我认为他是在引号中指错字,例如“终止”而不是“终止”。
迈克尔·科尔

3
@Michael实际上不是报价。我自己写的。
2011年

6

是的,这里使用的术语尚不清楚,但是无论如何,在大多数情况下,这些术语是一致的,所以这并不是一个太大的问题。

一个主要区别是对术语进行评估的时间。为此有多种策略,范围从“尽快”到“仅在最后一刻”。术语急于评价有时用于战略朝着前倾斜,而懒惰的评价恰当地指的是一系列严重依赖后者的策略。“惰性评估”与相关策略之间的区别往往涉及何时保留评估结果的时间和位置,而不是抛弃。Haskell中为数据结构分配名称并为其建立索引的熟悉的记忆技术基于此。相反,仅将表达式彼此拼接在一起的语言(如“按姓名呼叫”评估中那样)可能不支持此功能。

另一个区别是要评估哪些术语,范围从“绝对所有”到“尽可能少”。由于不能忽略实际用于计算最终结果的任何值,因此这里的区别是要计算多少个多余项。除了减少程序要做的工作量之外,忽略未使用的术语还意味着它们不会产生任何错误。当被有区别的,严格是指评估所考虑的一切的财产(以严格的功能的情况下,例如,这意味着它适用的条款,这并不一定参数内平均的子表达式) ,而非严格 意味着仅评估某些事物(通过延迟评估或完全放弃术语)。

应该很容易看到它们如何以复杂的方式相互作用。决策根本不是正交的,因为极端情况往往是不相容的。例如:

  • 非常严格的评估排除了一定程度的渴望;如果您不知道是否需要一个术语,则不能评估它。

  • 非常严格的评估使不急于变得不相关。如果您正在评估所有内容,那么何时进行的决定就没有那么重要了。

但是,确实存在其他定义。例如,至少在Haskell中,“严格函数”通常被定义为强制其自变量以使该函数在任何自变量出现时均求值为_ | _(“底部”)的函数。请注意,根据此定义,它id是严格的(从琐碎意义上来说),因为强制的结果与单独id x强制具有完全相同的行为x


非严格==“尽可能少” lazy ==“仅在最后一刻”。因此,短路(f1() || f2())将是非严格的,因为它只会一直评估到正确。回调函数或yield类似于lazy,因为它仅在“最后时刻”求值。(\x->1)非严格的也是如此,因为它评估“尽可能少”。(\x->1)(undefined)因为Haskell的求值是懒惰的,所以仅按表达式应用表达式才变得懒惰。然而,(\x->1)本身没有应用仅仅是非严格的。我在正确的轨道上吗?

1
@padawanHaskell:“惰性”通常表示更具体的内容(Haskell的方法多种多样)。因此,回调在一次只计算事物的意义上并不是“懒惰”的,而是以几乎所有语言都没有急切且不严格的方式进行,即在将它们应用到函数之前不会抢先评估内部函数。论点。短路是非限制性的一种非常有限的形式,通常会在其他非常严格的语言中找到。但是仍然存在短路但是,急切,因为它会立即评估或丢弃。
CA McCann,

1
@padawanHaskell:无论如何,在没有更多上下文的情况下谈论表达式是否/不渴望/严格并不总是有意义的。这些是语言的属性,而不是程序的属性。程序中编写的表达式并不是真正的惰性或非惰性,它只是存在。
CA McCann,

1
从某种意义上说,《 Haskell》中没有什么是急切的。事物要么是“惰性”,要么是“评估”,后者可能包含惰性子表达式。什么不同的是严谨,在要求别人一定表现形式进行评估,然后改变“懒”的东西是怎么必要性了。
CA McCann

我现在不急了。我认为不渴望就意味着懒惰,但这是错误的二分法。现在,我明白了为什么您说回调并不急切,但并不懒惰。他们的评价并不严格,但是他们没有分享他们的结果。Wiki定义将惰性定义为非严格评估和共享是有意义的。

5

最初是作为更新,但开始变得很长。

懒惰/按需呼叫是按名称呼叫的记忆版本,如果评估了函数参数,则该值将存储下来供以后使用。在“纯”(无效果)设置中,这产生的结果与“按名称呼叫”相同;当函数参数使用两次或更多次时,按需调用几乎总是更快。
命令性示例-显然这是可能的。有一篇有趣的文章,关于惰性命令式语言。它说有两种方法。一个要求关闭,第二个要求使用图形约简。由于C不支持闭包,因此您需要将参数显式传递给迭代器。您可以包装地图结构,如果该值不存在,请计算该结构,否则返回值。
注意:Haskell通过“代码指针在首次执行时被值替换”来实现这一点-luqui。
这不是严格的按名称呼叫,但具有共享/存储结果的功能。

Call-By-Name-按名称进行调用的评估中,在调用函数之前不评估函数的参数-而是将它们直接替换为函数主体(使用捕获避免替换),然后保留为只要它们出现在函数中就进行评估。如果函数主体中未使用参数,则永远不会对参数求值;如果多次使用,则每次出现时都会对其进行重新评估。
命令性示例:回调
注意:这是非严格限制,因为如果不使用它可以避免求值。

不严格=在非严格评估中,除非在函数主体的评估中实际使用了函数的自变量,否则不对其进行评估。
命令性示例:短路
注意:_ | _似乎是测试功能是否严格的一种方法

因此,功能可以是非严格的,但不能是懒惰的。惰性函数始终是非严格的。 需要呼叫部分由呼叫名称定义,部分由非严格定义

摘自“懒惰的命令式语言”

2.1。非严格语义学VS。惰性评估我们必须首先阐明“非严格语义”和“惰性评估”之间的区别。非严格语义是那些指定在原始操作需要表达式之前不对其求值的表达式。可能存在各种类型的非严格语义。例如,非严格过程调用在需要参数值之前不会对参数进行求值。数据构造函数可能具有非严格语义,其中复合数据由未评估的部分组合而成。惰性评估(也称为延迟评估)是通常用于实现非严格语义的技术。在第4节中,非常简要地总结了通常用于实现惰性评估的两种方法。

按值调用,按延迟调用和按名称调用“按值调用”是用于具有严格语义的过程调用的通用名称。在按值语言的调用中,过程调用之前将评估过程调用的每个参数;然后将值传递给过程或封闭表达式。按值调用的另一个名称是“急切”求值。按值调用也称为“应用顺序”求值,因为所有参数都在函数应用到它们之前进行求值。“按懒惰调用”(使用William Clinger在[8中的术语] ])是使用非严格语义的过程调用的名称。在通过延迟过程调用进行调用的语言中,在将参数替换为过程主体之前,不会对其进行评估。由于表达式的求值顺序(从最外到最内,从左到右),通过延迟求值的调用也称为“正常顺序”求值。“按名称调用”是Algol中使用的延迟调用的一种特殊实现。 -60 [18]。Algol-60的设计者希望在对主体进行评估之前,将按名称进行调用的参数以物理方式替换为过程主体,并在括号中加上适当的名称更改以避免冲突。

通过LAZY VS致电。按需调用按需调用是按懒调用的扩展,这是由于观察到,一旦强制执行,可以通过记住给定延迟表达式的值来优化惰性计算,这样就可以再次使用该值,而无需重新计算该值。因此,按需评估调用可以通过使用备注扩展惰性方法的调用,从而避免重复评估的需要。弗里德曼(Friedman)和怀斯(Wise)是按需评估的最早倡导者,他们提出了“自杀式中止”,当他们初次被评估时会自我毁灭,取而代之的是自己的价值观。


0

以我的理解,“非严格”意味着试图通过完成来减少工作量以较少的。

而“惰性评估”和类似的尝试通过避免完全完成来减少总体工作量(希望永远)。

从你的例子中...

f1() || f2()

...从该表达式短路可能不会导致触发“未来工作”,并且推理没有投机/摊销因素,也没有任何计算复杂性债务产生。

而在C#示例中,“惰性”在整体视图中保留了一个函数调用,但作为交换,它具有上述困难(至少从调用的角度直到可能完全完成...在此代码中都是可以忽略的,距离路径,但请想象这些功能具有一些高争用锁)。

int f1() { return 1;}
int f2() { return 2;}

int lazy(int (*cb1)(), int (*cb2)() , int x) {
    if (x == 0)
        return cb1();
    else
        return cb2();
}

int eager(int e1, int e2, int x) {
    if (x == 0)
         return e1;
    else
         return e2;
}

lazy(f1, f2, x);
eager(f1(), f2(), x);

-3

如果我们谈论的是通用计算机科学术语,那么“惰性”和“非严格”通常是同义词-它们代表着相同的整体思想,在不同情况下以不同的方式表达自己。

但是,在给定的特定专业环境中,它们可能获得了不同的技术含义。我认为关于“懒惰”和“非严格”之间的区别可能会发生什么区别,您无法准确准确地说出。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.