懒惰的评估
#python #haskell #lazyevaluation

nibble:一小块食物被咬了。在计算中:一半的信息。每个小动物都在五分钟内解释了一个计算科学或软件工程的想法。

每个编程语言都需要在哪种顺序评估表达式中进行选择。几乎所有使用严格的评估:在评估表达式之前,首先评估所有子表达。例如,在函数之前对函数的参数进行评估。许多人使用急切的评估作为严格评估的同义词。

少数族裔,最突出的haskell,使用 nontract评估。非严格的评估是由不是什么来定义的:任何不首先或根本不评估所有子表达的评估策略是非图案。 懒惰评估是非分数评估的特定策略。

让我们尝试用懒惰的评估来清除混乱。

koala on bough
我羡慕考拉斯在最尴尬的地方非常舒适的能力。 David Clode摄于Unsplash

严格的评估

像大多数语言一样,Python对功能应用程序使用严格的评估:

def log(value):
    if log_level == INFO:
        print(value)

log(42 + 33)

首先,计算总和,然后记录。虽然直观地理解,但该示例显示了严格评估的潜在缺点:如果该论点最终未使用,则如果log_levelWARN,则是不必要的。

发现严格评估的一种简单方法是想象一个子表达进入无限循环会发生什么。如果顶级表达式从未得到评估,则评估是严格的。

严格的评估不限于评估哪个顺序子表达,只是在顶级表达式之前对它们进行了评估。它可以像在Python一样向右。右至左,就像OCAML一样;或不确定的,例如c。

非严格评估

非严格的评估并不像首先出现的那样不寻常-if...else...是非图片。

让我们运用我们的技巧来发现严格的评估。以下Python程序即使永远终止了else分支循环:

if True:
    print("I am True")
else:
    while True:
        print("I never terminate")

根本没有评估else分支,因此if...else...在Python中并不严格。

短路布尔操作也是非图案。以下or表达式终止。

True or infinite_loop()

非严重的DIY

这很适合内置,但是如果您想做正确的事...

我们可以使用 thunks :无参数的功能来模拟非严格评估。延迟评估直到需要。

def log(value_thunk):
    if log_level == INFO:
        print(value_thunk())

log(lambda: 42 + 33)

thunks避免了不必要的工作 - 如果登录的值昂贵,则log函数仅在启用日志记录时对其进行评估。

懒惰评估

thunks的一个烦人的问题是,每次被称为它们。

def log(value_thunk):
    if log_level == INFO:
        print(value_thunk())
        send_to_log_aggregator(value_thunk())

为了解决这个问题,我们可以在第一次进行评估时将其结果保留,并重新使用它。这种评估策略称为懒惰评估。它结合了两个优化:

  • 它永远不会通过延迟评估直至需要。

  • 它永远不会通过缓存第一个结果来重复工作。

有一个捕获:如果thunk具有副作用,例如写入磁盘,则很难理解何时或是否发生副作用。因此,懒惰的评估假设Thunks没有副作用。

功能编程语言Haskell默认使用懒惰评估,因为它是纯粹的:Haskell功能没有副作用。

使用lazy关键字或Lazy对象等严格的语言,例如OCAML,F#和C#支持懒惰评估。

这是python中Lazy对象的说明性实现:

class Lazy:
    def __init__ (self, thunk):
        self._thunk = thunk
        self._is_cached = False
        self._value = None

    @property
    def value(self):
        if not self._is_cached:
            self._value = self._thunk()
            self._is_cached = True
        return self._value

为什么懒惰评估很重要

严格的评估更为普遍,因为它更容易理解,并且更简单地调试。也就是说,在某些情况下,非严格的评估是必不可少的。

道格·麦克罗伊(Doug Mcilroy

这是一行:

series f = f : repeat 0

在Haskell中,基本数据结构之一是懒惰列表。函数series创建了这样的列表。列表的第一个元素是数字f,其余的为零。列表构造函数:用一个元素预先列出列表。标准函数repeat s创建了一个无限的s

具体地,series 13代表无限列表[13, 0, 0, 0,...]。但是由于它很懒惰,因此只有在需要时计算元素。要求元素的一种方法是使用函数take n,该函数从列表的正面获取n元素。换句话说,take 4 (series 13)产生[13, 0, 0, 0]

在这一点上,您可能会认为懒惰列表只是迭代器。像懒惰列表一样,迭代器按需计算,可用于表示无限的数据结构。这是series作为python发电机:

def series(f: int) -> Iterable[int]:
    yield f
    while True:
        yield 0

区别在于懒惰列表在评估列表时的缓存元素。类似的程序:

myList = series 10 --list thunk is created
some = take 5 myList --eval first 5 elements
somemore = take 20 myList --eval 15 more elements

总共只评估20个值。有时无法重置迭代器,如果可以,整个计算是从头开始重做的。懒惰列表不是这样!

重新恢复严重的力量。道格·麦克罗伊(Doug McIlroy)使用懒惰列表来表示无限动力系列的系数。换句话说,列表[1, 2, 3, 4, 0, ...]表示功率系列1 +2ð¥ +3ð¥â€² +4ð¥ - + ...

这是两个电源系列的添加:

 (f:ft) + (g:gt) = f+g : ft+gt

Haskell的语法是简短的。在这一行中,:+符号都超载。等等标志左侧的:符号表示列表解构。在右边,这意味着列表构造。这是颜色编码每个符号的不同含义以澄清的图像:

Image description

保留 - 没有基本情况的递归定义?由于懒惰的评估,这只有可能。当需要两个系列的第一个元素时,Express f+g依次需要fg,它们通过懒惰模式触发左和右列表的第一个元素的评估与(f:ft)(g:gt)匹配。延迟对ft+gt的进一步评估,直到需要更多元素。当要求第二个元素等时,此过程重复。

懒惰的无限列表简化了一切:无需检查列表何时结束,因为它没有,并且不需要递归基本情况,因为我们只需要在需要时重新浏览。

回顾

这个小麻烦讨论了一堆术语。

a venn diagram showing strict, non strict and lazy evaluation

i将评估策略分为两类:严格和非刻板。

在严格的评估中,首先评估所有表达式的子表达。非严格的评估包括所有其他任何事情的策略,包括根本不评估某些论点。

严格的评估有时称为急切的评估,这听起来像是懒惰评估的对应物。但是,懒惰的评估只是许多可能的非严重策略之一。懒惰评估之所以脱颖而出,是因为它另外缓存了结果,但是只有在表达没有副作用的情况下才有可能。

最后,严格和非分数评估通常以单个编程语言甚至相同的表达方式一起使用。因此,图像中的相交。一个示例是if...else...:条件的评估是严格的,但是对这两个子句的评估是非图案。

感谢您的阅读!我打算每个月写一个刺。有关更多信息,subscribe to my newletterfollow me on Twitter