懒惰求值与宏的比较
懒惰求值与宏的比较
我已经习惯了Haskell的惰性求值,现在使用默认热切求值的语言时发现自己变得很烦躁。这实际上是很损害的,因为我使用的其他语言往往使惰性求值变得很不方便,通常需要使用自定义迭代器等等。所以仅仅通过获取一些知识,我实际上使自己在原始语言中的生产力降低了。
但是我听说AST宏提供了另一种干净的方法来完成同样的事情。我经常听到像“惰性求值使宏变得多余”和“反之亦然”这样的说法,大部分来自战斗的Lisp和Haskell社区。
我尝试过使用各种Lisp变种进行宏编程。他们似乎只是在编译时整理和复制代码块的一种很有条理的方法。它们肯定不是Lisp程序员所认为的圣杯。但几乎可以肯定的是,这是因为我无法很好地使用它们。当然,宏系统在语言本身组装的同一核心数据结构上工作非常有用,但它仍然基本上是一种组织代码的复制和粘贴的方法。我承认,在基于允许完全运行时修改的语言的相同AST上构建宏系统是强大的。
我想知道的是,如何使用宏来简洁地做到惰性求值做的事情?如果我想一行一行地处理文件而不是糊糊整个文件,我只需返回一个已经经过一行读取例程映射的列表。这是完美的DWIM(做我所想)的例子。我甚至不需要考虑它。
我很明显不懂宏。我使用过它们,但并没有受到炒作的特别印象。所以有些东西我没有从在线文档中阅读到。有人能向我解释这一切吗?
延迟求值可以替代某些宏的使用(那些延迟求值以创建控制结构),但反过来并不完全正确。您可以使用宏使延迟求值构造更透明 - 例如,请参见SRFI 41(流)的示例:http://download.plt-scheme.org/doc/4.1.5/html/srfi-std/srfi-41/srfi-41.html
此外,您还可以编写自己的惰性IO原语。
根据我的经验,在严格语言中普遍采用惰性代码会引入开销,而不是在运行时设计为从一开始就有效支持惰性代码 - 其中,您需要注意的是实施问题。
惰性求值使宏无用
这完全是无稽之谈(不是你的错,我以前听过这种说法)。确实可以使用宏改变表达式求值的顺序、上下文等,但这只是宏的最基本用途,用临时宏模拟惰性语言而不是函数远远不方便。因此,如果您从这个角度来看宏,您确实会感到失望。
宏用于通过新的语法形式扩展语言。一些宏的特定功能包括:
- 影响表达式求值的顺序、上下文等。
- 创建新的绑定形式(即影响表达式所在的范围)。
- 执行编译时计算,包括代码分析和转换。
做(1)的宏可能非常简单。例如,在Racket中,异常处理表单with-handlers
只是一个将其扩展为call-with-exception-handler
、一些条件语句和一些连续代码的宏。用法如下:
(with-handlers ([(lambda (e) (exn:fail:network? e)) (lambda (e) (printf "network seems to be broken\n") (cleanup))]) (do-some-network-stuff))
这个宏基于原始的call-with-exception-handler
实现了“在异常的动态上下文中的谓词和处理程序子句”的概念,后者会在抛出异常时处理所有异常。
使用宏的更复杂的例子是实现LALR(1)解析器生成器。它不需要预处理的单独文件,而是只是另一种类型的表达式。它接受语法描述,计算编译时的相关表格,生成解析器函数。操作例程是词法作用域的,因此它们可以引用文件中的其他定义甚至是lambda
绑定的变量。它甚至可以在操作例程中使用其他语言扩展。
在极端情况下,Typed Racket 是一个通过宏实现的带类型的 Racket 方言。它拥有一个精密的类型系统,旨在匹配 Racket/Scheme 代码的习惯用法,通过使用动态软件契约(也是通过宏实现)来保护有类型的函数与无类型的模块进行互操作。它是通过“类型化模块”宏实现的,这个宏会展开、类型检查和转换模块体,以及辅助宏,用于将类型信息附加到定义等。
此外,还有Lazy Racket,一种惰性的 Racket 方言。它并不是通过把每个函数都变成宏来实现的,而是通过重新绑定lambda
、define
和函数应用语法为创建和强制实现承诺的宏。
总之,惰性评估和宏只有一个小交点,但它们是极其不同的东西。而宏肯定不会被惰性评估所包含。