关于haskell:懒惰评估与宏

Lazy Evaluation vs Macros

我习惯于从haskell进行懒惰的评估,并且发现我已经正确地使用了懒惰的评估,现在我对默认语言的热切感到恼火。这实际上是非常具有破坏性的,因为我使用的其他语言主要使懒散地评估东西非常尴尬,通常涉及到定制迭代器的推出等等。因此,仅仅通过获得一些知识,我实际上已经使自己在原始语言中的工作效率降低了。叹息。

但我听说AST宏提供了另一种干净的方法来做同样的事情。我经常听到"懒惰的评估会使宏冗余",反之亦然,这主要来自于与Lisp和Haskell社区的争吵。

我涉足过各种Lisp变体中的宏。它们看起来像是一种真正有组织的复制和粘贴代码块的方法,在编译时处理。他们当然不是利斯贝尔斯认为的圣杯。但这几乎是肯定的,因为我不能正确使用它们。当然,让宏系统在语言本身所装配的核心数据结构上工作是非常有用的,但它基本上仍然是一种有组织的复制和粘贴代码的方式。我承认,基于与允许完全运行时更改的语言相同的AST的宏系统是强大的。

我想知道的是,宏是如何被用来简洁而简洁地执行懒惰的计算的?如果我想一行一行地处理一个文件而不拖泥带水的话,我只需返回一个列表,上面映射了一个行读取例程。这是dwim的完美例子(按我的意思做)。我甚至不必考虑。

我显然没有宏。我用过它们,但在炒作中没有特别的印象。所以有一些东西我错过了,我不能通过在线阅读文档得到。有人能向我解释这一切吗?


Lazy evaluation makes macros redundant

这完全是胡说八道(不是你的错,我以前听说过)。的确,您可以使用宏来更改表达式计算的顺序、上下文等,但这是宏最基本的用法,使用特殊宏而不是函数来模拟懒惰的语言真的不方便。所以,如果你从这个方向来研究宏,你肯定会失望的。

宏用于用新的语法形式扩展语言。宏的某些特定功能是

  • 影响表达式计算的顺序、上下文等。
  • 创建新的绑定形式(即影响表达式的计算范围)。
  • 执行编译时计算,包括代码分析和转换。
  • 执行(1)的宏可以非常简单。例如,在racket中,异常处理形式with-handlers只是一个宏,它可以扩展为call-with-exception-handler、一些条件和一些延续代码。使用方法如下:

    1
    2
    3
    4
    5
    6
    (with-handlers ([(lambda (e) (exn:fail:network? e))
                     (lambda (e)
                       (printf"network seems to be broken
    "
    )
                       (cleanup))])
      (do-some-network-stuff))

    宏基于原语call-with-exception-handler,实现了"异常动态上下文中的谓词和处理程序子句"的概念,该原语在异常出现时处理所有异常。

    更复杂的宏使用是LALR(1)解析器生成器的实现。与需要预处理的单独文件不同,parser表单只是另一种表达式。它接受语法描述,在编译时计算表,并生成一个解析器函数。动作例程在词汇上是有范围的,因此它们可以引用文件中的其他定义,甚至可以引用lambda绑定变量。您甚至可以在操作例程中使用其他语言扩展。

    在最末端,类型化的racket是通过宏实现的一种类型化的racket方言。它有一个复杂的类型系统,设计来匹配Racket/Scheme代码的习惯用法,它通过使用动态软件契约(也通过宏实现)保护类型化函数来与非类型化模块进行交互。它由一个"类型化模块"宏实现,该宏扩展、类型检查并转换模块主体以及用于将类型信息附加到定义等的辅助宏。

    还有懒惰的球拍,一种懒惰的球拍方言。它不是通过将每个函数转换为宏来实现的,而是通过将lambdadefine和函数应用程序语法重新绑定到创建和强制承诺的宏来实现的。

    总之,懒惰的评估和宏有一个小的交叉点,但它们是非常不同的东西。宏当然不会被懒惰的计算所包含。


    懒惰是象征性的,而宏则不是。更准确地说,如果向表示性语言添加非严格性,则结果仍然是表示性的,但如果添加宏,则结果不是表示性的。换句话说,在懒惰的纯语言中,表达式的意义仅仅是组件表达式意义的函数;而宏可以从语义相等的参数中产生语义上不同的结果。

    从这个意义上讲,宏更强大,而懒惰则相应地在语义上表现得更好。

    编辑:更准确地说,宏是非表示性的,除了关于标识/琐碎的表示(其中"表示性"的概念变得空虚)。


    懒惰的计算可以代替宏的某些使用(那些延迟计算以创建控件构造的宏),但反过来并不是真的。您可以使用宏使延迟的评估构造更加透明--请参阅srfi 41(流),以了解如何执行以下操作的示例:http://download.plt-scheme.org/doc/4.1.5/html/srfi-std/srfi-41/srfi-41.html

    除此之外,您还可以编写自己的懒惰IO原语。

    然而,根据我的经验,与运行时中普遍懒惰的代码相比,严格语言中普遍懒惰的代码往往会引入开销,而运行时设计的从一开始就有效地支持它——记住,这确实是一个实现问题。


    Lisp始于上个千年的50年代末。参见符号表达式的递归函数及其计算机计算。宏不是该Lisp的一部分。其思想是用符号表达式来计算,符号表达式可以表示各种公式和程序:数学表达式、逻辑表达式、自然语言句子、计算机程序……

    后来,Lisp宏被发明了,它们是上述思想在Lisp本身中的应用:宏使用完整的Lisp语言作为转换语言,将Lisp(或Lisp-like)表达式转换为其他Lisp表达式。

    您可以想象,使用宏,您可以作为Lisp的用户实现强大的预处理器和编译器。

    典型的Lisp方言使用严格的参数评估:函数的所有参数都在函数执行之前进行评估。Lisp还有几个内置的表单,它们具有不同的评估规则。IF就是这样一个例子。通常,lisp IF是一个所谓的特殊运算符。

    但是我们可以定义一种新的类似于Lisp的(子)语言,它使用懒惰的计算,我们可以编写宏来将该语言转换为Lisp。这是一个宏应用程序,但迄今为止还不是唯一一个。

    使用宏来实现代码转换器的此类Lisp扩展的一个示例(相对较旧),该代码转换器为数据结构提供延迟评估,它是通用Lisp的系列扩展。


    宏可以用来处理懒惰的计算,但它只是其中的一部分。宏的要点是,由于它们,语言中基本上没有固定的内容。

    如果编程就像玩乐高积木,使用宏还可以更改积木的形状或使用的材料。

    宏不仅仅是延迟计算。这是可用的fexpr(Lisp历史上的一个宏观前兆)。宏是关于程序重写的,其中fexpr只是一个特例…

    举个例子,我在业余时间编写了一个很小的lisp-to-javascript编译器,最初(在javascript内核中)我只有lambda支持&rest参数。现在有了对关键字参数的支持,这是因为我重新定义了lambda在Lisp本身中的含义。

    我现在可以写:

    1
    (defun foo (x y &key (z 12) w) ...)

    调用函数时

    1
    (foo 12 34 :w 56)

    执行该调用时,函数体中的w参数将绑定到56,而z参数将绑定到12,因为它没有被传递。如果向函数传递了不支持的关键字参数,我还将得到一个运行时错误。我甚至可以通过重新定义编译表达式的含义来添加一些编译时检查支持(即,如果"静态"函数调用窗体正在向函数传递正确的参数,则添加检查)。

    中心点是原始(内核)语言根本不支持关键字参数,我可以使用该语言本身添加它。结果就像它从一开始就存在;它只是语言的一部分。

    语法很重要(即使技术上可以只使用图灵机)。句法塑造了你的思想。宏(和读取宏)使您可以完全控制语法。

    关键的一点是代码重写代码不使用残缺的Buff-Real-Primk*k类语言作为C++模板元编程(仅制作EDCOX1×5)是一个惊人的成就,或者用一个偶数笨拙小于ReXEP替代引擎如C预处理器。

    代码重写代码使用相同的完整(和可扩展)语言。一路上都是Lisp;-)

    当然,编写宏比编写常规代码更困难;但这是问题的"本质复杂性",而不是人为的复杂性,因为您被迫使用类似于C++元编程的哑半语言。

    编写宏比较困难,因为代码是一件复杂的事情,当编写宏时,编写的是复杂的东西,这些东西本身就是复杂的东西。更高一个级别并编写生成宏的宏(这是Lisp的老笑话"我正在编写编写编写编写代码的代码,而这些代码是为我付费的")的由来),这一点也不少见。

    但宏观力量是无边无际的。