【问题标题】:How does Lisp let you redefine the language itself?Lisp 如何让你重新定义语言本身?
【发布时间】:2011-01-19 10:29:34
【问题描述】:

我听说 Lisp 可以让你重新定义语言本身,我也尝试过研究它,但没有任何明确的解释。谁有一个简单的例子?

【问题讨论】:

  • 你读过哪本关于 LISP 的书没有涉及宏?
  • 关于 Lisp 和宏的许多其他 SO 问题涵盖了相同的领域:stackoverflow.com/questions/267862/…
  • 应该重新打开:Lisp 提供的不仅仅是宏来重新定义语言:读取宏、一等函数、函数建议、CLOS 元对象协议、CLOS 方法组合等等。
  • 请重新打开。我需要知道这件事。

标签: lisp


【解决方案1】:

Lisp 用户将 Lisp 称为可编程编程语言。它用于符号计算 - 用符号计算。

宏只是利用符号计算范式的一种方式。更广阔的视野是 Lisp 提供了描述符号表达式的简单方法:数学术语、逻辑表达式、迭代语句、规则、约束描述等等。宏(Lisp 源形式的转换)只是符号计算的一种应用。

这有一些方面:如果您询问“重新定义”语言,那么严格重新定义将意味着重新定义一些现有的语言机制(语法、语义、语用学)。但也有语言特征的扩展、嵌入、去除。

在 Lisp 传统中,已经有很多尝试提供这些功能。 Lisp 方言和特定实现可能只提供其中的一部分。

一些重新定义/更改/扩展主要 Common Lisp 实现所提供的功能的方法:

  • s 表达式语法。 s 表达式的语法不固定。读取器(函数 READ)使用所谓的读取表来指定读取字符时将执行的函数。可以修改和创建读取表。例如,这允许您更改列表、符号或其他数据对象的语法。还可以为新的或现有的数据类型(如哈希表)引入新语法。也可以完全替换 s-expression 语法并使用不同的解析机制。如果新的解析器返回 Lisp 形式,则解释器或编译器不需要更改。一个典型的例子是可以读取中缀表达式的读取宏。在这样的读取宏中,使用了运算符的中缀表达式和优先规则。读宏不同于普通宏:读宏工作在 Lisp 数据语法的字符级别。

  • 替换函数。顶级函数绑定到符号。用户可以更改 this 绑定。大多数实现都有一种机制,即使对于许多内置函数也是如此。如果您想提供内置函数 ROOM 的替代方案,您可以替换它的定义。一些实现会引发错误,然后提供继续更改的选项。有时需要解锁包裹。这意味着通常可以用新定义替换函数。这是有局限性的。一是编译器可以在代码中内联函数。要查看效果,需要重新编译使用更改代码的代码。

  • 建议功能。通常人们想为函数添加一些行为。这在 Lisp 世界中被称为“建议”。许多 Common Lisp 实现都会提供这样的工具。

  • 自定义包。包对名称空间中的符号进行分组。 COMMON-LISP 包是作为 ANSI Common Lisp 标准一部分的所有符号的所在地。程序员可以创建新包并导入现有符号。因此,您可以在您的程序中使用提供更多或不同功能的 EXTENDED-COMMON-LISP 包。只需添加 (IN-PACKAGE "EXTENDED-COMMON-LISP"),您就可以开始使用您自己的 Common Lisp 扩展版本进行开发。根据使用的命名空间,您使用的 Lisp 方言可能看起来略有不同,甚至完全不同。在 Lisp 机器上的 Genera 中,有几种 Lisp 方言以这种方式并排排列:ZetaLisp、CLtL1、ANSI Common Lisp 和 Symbolics Common Lisp。

  • CLOS 和动态对象。 Common Lisp 对象系统带有内置的更改。元对象协议扩展了这些能力。 CLOS 本身可以在 CLOS 中扩展/重新定义。你想要不同的继承。写一个方法。您需要不同的方式来存储实例。写一个方法。插槽应该有更多的信息。为此提供一个类。 CLOS 本身的设计使其能够实现不同面向对象编程语言的整个“区域”。典型的例子是添加原型,与外部对象系统(如 Objective C)集成,添加持久性,...

  • Lisp 形式。 Lisp 形式的解释可以用宏重新定义。宏可以解析它所包含的源代码并对其进行更改。有多种方法可以控制转换过程。复杂的宏使用 code walker,它理解 Lisp 表单的语法并可以应用转换。宏可以是微不足道的,但也可以像 LOOP 或 ITERATE 宏一样变得非常复杂。其他典型示例是用于嵌入式 SQL 和嵌入式 HTML 生成的宏。宏也可以用来将计算转移到编译时间。由于编译器本身就是一个 Lisp 程序,因此可以在编译期间进行任意计算。例如,如果某些参数在编译期间已知,则 Lisp 宏可以计算公式的优化版本。

  • 符号。 Common Lisp 提供符号宏。符号宏允许更改源代码中符号的含义。一个典型的例子是:(with-slots (foo) bar (+ foo 17)) 这里用 WITH-SLOTS 括起来的源代码中的符号 FOO 将被替换为调用 (slot-value bar 'foo)。

  • 优化,使用所谓的编译器宏可以提供更高效的某些功能版本。编译器将使用那些编译器宏。这是用户进行程序优化的有效方式。

  • 条件处理 - 处理因以某种方式使用编程语言而产生的条件。 Common Lisp 提供了一种处理错误的高级方法。条件系统还可用于重新定义语言特征。例如,可以使用一种自写的自动加载机制来处理未定义的函数错误。当 Lisp 看到未定义的函数时,错误处理程序可以尝试自动加载函数并在加载必要的代码后重试操作,而不是登陆调试器。

  • 特殊变量 - 将变量绑定注入现有代码。许多 Lisp 方言,如 Common Lisp,提供特殊/动态变量。它们的值在运行时在堆栈上查找。这允许封闭代码添加影响现有代码的变量绑定而不更改它。一个典型的例子是像 *standard-output* 这样的变量。可以重新绑定变量,并且在新绑定的动态范围内使用此变量的所有输出都将转向新的方向。 Richard Stallman 认为这对他来说非常重要,因为它在 Emacs Lisp 中被默认设置(尽管 Stallman 知道 Scheme 和 Common Lisp 中的词法绑定)。

Lisp 拥有这些以及更多的功能,因为它已被用于实现许多不同的语言和编程范例。一个典型的例子是逻辑语言的嵌入式实现,比如 Prolog。 Lisp 允许使用 s 表达式和特殊的编译器来描述 Prolog 术语,Prolog 术语可以编译为 Lisp 代码。有时需要通常的 Prolog 语法,然后解析器会将典型的 Prolog 术语解析为 Lisp 形式,然后将其编译。嵌入式语言的其他示例包括基于规则的语言、数学表达式、SQL 术语、内联 Lisp 汇编器、HTML、XML 等等。

【讨论】:

    【解决方案2】:

    在定义新语法时,我将指出 Scheme 与 Common Lisp 不同。它允许您使用define-syntax 定义模板,这些模板将应用于您的源代码,无论它们在哪里使用。它们看起来就像函数,只是它们在编译时运行并转换 AST。

    下面是一个示例,说明如何根据lambda 定义letlet 行是要匹配的模式,lambda 行是生成的代码模板。

    (define-syntax let
      (syntax-rules ()
        [(let ([var expr] ...) body1 body2 ...)
         ((lambda (var ...) body1 body2 ...) expr ...)]))
    

    请注意,这与文本替换完全不同。您实际上可以重新定义lambda,上面对let 的定义仍然有效,因为它在定义let 的环境中使用lambda 的定义。基本上,它像宏一样强大,但功能却很干净。

    【讨论】:

      【解决方案3】:

      宏是这样说的通常原因。这个想法是因为代码只是一个数据结构(或多或少是一棵树),你可以编写程序来生成这个数据结构。因此,您所知道的有关编写生成和操作数据结构的程序的所有知识,都会增加您的表达能力。

      宏并不是对语言的完全重新定义,至少据我所知(我实际上是一个计划者;我可能是错的),因为有一个限制。宏只能获取代码的单个子树,并生成单个子树来替换它。因此,你不能编写整个程序转换的宏,那样会很酷。

      但是,就目前的情况而言,宏仍然可以做很多事情 - 绝对比任何其他语言都能让您做的事情更多。而且如果你使用的是静态编译,那么做一个全程序转换一点也不难,那么限制就不是什么大问题了。

      【讨论】:

      • “代码就是数据”的格言可能只在早期的实现中是正确的,但是现在关于包和封闭环境的细节太多了,所以它不再只是列表中的符号,而是变量在抽象语法树中——即 code.
      【解决方案4】:

      我在答案中缺少对“计算机程序的结构和解释”第 4-5 章的引用 (link)。

      这些章节将指导您在 Lisp 中构建 Lisp 评估器。我喜欢这本书,因为它不仅展示了如何在新的评估器中重新定义 Lisp,而且还让你了解了 Lisp 编程语言的规范。

      【讨论】:

        【解决方案5】:

        此答案专门针对 Common Lisp(以下称为 CL),尽管部分答案可能适用于 lisp 家族中的其他语言。

        由于 CL 使用 S 表达式并且(大部分)看起来像一系列函数应用程序,因此内置代码和用户代码之间没有明显区别。主要区别在于“语言提供的东西”在编码环境中的特定包中可用。

        稍加注意,编写替换代码并使用它们并不难。

        现在,“普通”阅读器(读取源代码并将其转换为内部符号的部分)期望源代码采用相当特定的格式(带括号的 S 表达式),但由于阅读器是由名为“read-tables”,这些可以由开发人员创建和修改,也可以更改源代码的外观。

        这两件事至少应该为为什么 Common Lisp 可以被认为是一种可重新编程的编程语言提供一些理由。我手头没有一个简单的例子,但我有一个将 Common Lisp 翻译成瑞典语的部分实现(创建于几年前的 4 月 1 日)。

        【讨论】:

          【解决方案6】:

          从外面看……

          我一直认为这是因为 Lisp 在其核心提供了这种基本的原子逻辑运算符,因此可以从基本组件构建(并且已经构建并作为工具集和插件提供)任何逻辑过程。

          与其说它可以重新定义自己,不如说它的基本定义是如此具有延展性,以至于它可以采用任何形式,并且没有任何形式被假定/假定到结构中。

          打个比方,如果你只有有机化合物,你就做有机化学,如果你只有金属氧化物,你就做冶金,但如果你只有元素,你可以做任何事情,但你需要完成额外的初始步骤......大多数其中其他人已经为您完成了....

          我觉得.....

          【讨论】:

            【解决方案7】:

            http://www.cs.colorado.edu/~ralex/papers/PDF/X-expressions.pdf 上的酷示例

            阅读器宏定义 X 表达式与 S 表达式共存,例如,

            ? (cx <circle cx="62" cy="135" r="20"/>) 
            62
            

            plain vanilla Common Lisp http://www.AgentSheets.com/lisp/XMLisp/XMLisp.lisp ...

            (eval-when (:compile-toplevel :load-toplevel :execute)
              (when (and (not (boundp '*Non-XMLISP-Readtable*)) (get-macro-character #\<))
                (warn "~%XMLisp: The current *readtable* already contains a #/< reader function: ~A" (get-macro-character #\<))))
            

            ... XML 解析器当然不是那么简单,但是将其连接到 lisp 阅读器中就很简单了。

            【讨论】:

              猜你喜欢
              • 2022-06-14
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-07-07
              • 1970-01-01
              相关资源
              最近更新 更多