我习惯于从Haskell进行懒惰的评估,并且发现自己因为我已经正确地使用了懒惰的评估而感到厌烦。这实际上是非常具有破坏性的,因为我使用的其他语言主要是懒得评估一些非常笨拙的东西,通常涉及自定义迭代器的推出等等。因此,通过获取一些知识,我实际上已经使自己 在我的原始语言中高效。叹息。
但是我听说AST宏提供了另一种干净的方式来做同样的事情。我经常听到诸如“懒惰评估使宏多余”之类的陈述,反之亦然,主要是来自对抗Lisp和Haskell社区。 p>
我已经涉及各种Lisp变种中的宏。它们看起来像是一种非常有组织的复制和粘贴代码块的方式,可以在编译时处理。他们当然不是Lispers认为的圣杯。但这几乎可以肯定是因为我无法正确使用它们。当然,让宏系统在与语言本身组合在一起的相同核心数据结构上工作是非常有用的,但它仍然基本上是一种复制和粘贴代码的有组织方式。我承认,基于与允许完全运行时更改的语言相同的AST的宏系统是强大的。
我想知道的是,如何使用宏来简明扼要地进行懒惰评估呢?如果我想逐行处理文件而不会搞砸整个事情,我只返回一个列表,其中有一个映射到它的行读取例程。这是DWIM的完美例子(尽我所能)。我甚至不必考虑它。
我显然不会得到宏。我已经使用过它们并且在炒作时并没有特别留下深刻的印象。因此,我缺少一些我没有通过在线阅读文档获得的东西。有人可以向我解释所有这些吗?
答案 0 :(得分:59)
延迟评估会使宏变得多余
这纯粹是胡说八道(不是你的错;我之前听过)。确实,您可以使用宏来更改表达式求值的顺序,上下文等,但这是宏的最基本用法,使用ad-hoc宏而不是函数来模拟惰性语言真的不方便。所以,如果你从那个方向来到宏,你的确会感到失望。
宏用于使用新的语法形式扩展语言。宏的一些特定功能是
执行(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
实现了“异常的动态上下文中的谓词和处理程序子句”的概念,原始parser
在它们被引发时处理所有异常。
更复杂的宏的使用是LALR(1) parser generator的实现。 lambda
表单只是另一种表达式,而不是需要预处理的单独文件。它需要语法描述,在编译时计算表,并生成解析器函数。动作例程是词法范围的,因此它们可以引用文件中的其他定义甚至是lambda
- 绑定变量。您甚至可以在动作例程中使用其他语言扩展。
在最末端,Typed Racket是通过宏实现的Racket的类型方言。它有一个复杂的类型系统,旨在匹配Racket / Scheme代码的习语,并通过动态软件合同(也通过宏实现)保护类型函数,与非类型模块互操作。它由一个“类型模块”宏实现,它扩展,类型检查和转换模块体以及辅助宏,用于将类型信息附加到定义等。
FWIW,还有Lazy Racket,一种懒惰的方格球拍。它不是通过将每个函数转换为宏来实现的,而是通过将define
,{{1}}和函数应用程序语法重新绑定到创建和强制使用的宏来实现。
总之,懒惰的评估和宏有一个小的交叉点,但它们是非常不同的东西。并且宏观肯定不会被惰性评估所包含。
答案 1 :(得分:22)
延迟评估可以替代宏的某些用途(延迟评估以创建控制结构的那些),但反过来却不是真的。您可以使用宏来使延迟评估结构更加透明 - 有关如何:http://download.plt-scheme.org/doc/4.1.5/html/srfi-std/srfi-41/srfi-41.html
的示例,请参阅SRFI 41(Streams)除此之外,您还可以编写自己的懒惰IO原语。
然而,根据我的经验,普遍使用严格语言的懒惰代码往往会带来开销,而运行时的普遍延迟代码旨在从一开始就有效地支持它 - 请注意,这实际上是一个实现问题
答案 2 :(得分:22)
懒惰是denotative,而宏不是。 更准确地说,如果您将非严格性添加到外延语言中,结果仍然是指示性的,但如果添加宏,则结果不是指示性的。 换句话说,惰性纯语言中表达式的含义仅仅是组件表达式的含义;而宏可以从语义上相等的参数产生语义上不同的结果。
从这个意义上说,宏更强大,而懒惰在语义上相应表现得更好。
编辑:更准确地说,宏是非指示性的除了以外的身份/琐事(“外延”的概念变成空的)。
答案 3 :(得分:9)
Lisp始于上一个千年的50年代后期。见RECURSIVE FUNCTIONS OF SYMBOLIC EXPRESSIONS AND THEIR COMPUTATION BY MACHINE。宏不是Lisp的一部分。这个想法是用符号表达式计算,它可以代表各种公式和程序:数学表达式,逻辑表达式,自然语言句子,计算机程序,......
后来发明了Lisp宏,它们是上述想法应用于Lisp本身:宏使用完整的Lisp语言作为转换语言将Lisp(或类似Lisp)表达式转换为其他Lisp表达式。
您可以想象,使用Macros,您可以作为Lisp的用户实现强大的预处理器和编译器。
典型的Lisp方言使用严格的参数评估:在函数执行之前评估函数的所有参数。 Lisp还有几种具有不同评估规则的内置表单。 IF
就是这样一个例子。 Common Lisp IF
是一个所谓的特殊运算符。
但我们可以定义一个新的类似Lisp的语言,使用延迟评估,我们可以编写宏来将该语言转换为Lisp。这是宏的应用程序,但到目前为止还不是唯一的。
使用宏实现代码转换器的Lisp扩展示例(相对较旧),该代码转换器为惰性求值提供数据结构是Common Lisp的SERIES扩展。
答案 4 :(得分:5)
宏可用于处理延迟评估,但只是其中的一部分。宏的主要观点是,归功于它们,基本上没有任何语言固定。
如果编程就像玩乐高积木一样,使用宏还可以改变砖块的形状或它们构建的材料。
宏不只是延迟评估。这可以作为fexpr
(在lisp历史中的宏观前兆)获得。宏是关于程序重写的,其中fexpr
只是一个特例......
作为一个例子考虑我在业余时间写一个小的lisp到javascript编译器,最初(在javascript内核中)我只有lambda支持&rest
参数。现在支持关键字参数,因为我重新定义了lambda在lisp中的含义。
我现在可以写:
(defun foo (x y &key (z 12) w) ...)
并使用
调用该函数(foo 12 34 :w 56)
执行该调用时,在函数体中,w
参数将绑定到56,z
参数将绑定到12,因为它未被传递。如果将不受支持的关键字参数传递给函数,我也会收到运行时错误。我甚至可以通过重新定义编译表达式的方法来添加一些编译时检查支持(即添加检查“静态”函数调用表单是否将正确的参数传递给函数)。
中心点是原始(内核)语言根本不支持关键字参数,我能够使用语言本身添加它。结果就像从一开始就在那里;它只是语言的一部分。
语法很重要(即使技术上可以使用图灵机)。语法塑造了你的想法。宏(和读取宏)使您可以完全控制语法。
关键的一点是,代码重写代码并没有像C ++模板元编程那样使用残缺乏力的大脑类似语言(其中只是使if
成为一项了不起的成就),或者使用了甚至像C预处理器一样使用低于正则表达式的替代引擎。
代码重写代码使用相同的完整(和可扩展)语言。它一直是低迷的; - )
确实编写宏比编写常规代码更难;但这是问题的“基本复杂性”,而不是人为的复杂性,因为你被迫使用像C ++元编程这样的半哑语言。
编写宏更难,因为代码是一个复杂的东西,在编写宏时,你会编写复杂的东西,自己构建复杂的东西。甚至不是更常见的是上升一级并编写宏生成宏(这就是旧的lisp笑话“我正在编写编写代码的代码,这些代码是我付费的代码”来自)。 / p>
但宏观力量无限。