LISP宏能够达到什么程度?

50

我读到很多关于LISP可以通过宏动态地重新定义语法的内容。我很好奇这到底有多远?你能否重新定义语言结构,使其边缘化成为另一种语言的编译器?例如,你能否将LISP的功能性改变为更接近面向对象的语法和语义,比如更接近Ruby的语法?

特别是,使用宏是否可能消除括号混乱?我已经学会了足够的(Emacs-)LISP来定制我的微小功能,但我非常好奇宏在定制语言方面能达到什么程度。


1
Scheme宏非常强大。我曾经写过一个完整的LINQ实现作为Scheme宏。https://ironscheme.svn.codeplex.com/svn/IronScheme/IronSchemeConsole/ironscheme/linq.ss 有了这么多的能力,我已经没有好的想法来应用它了! - leppie
2
Peter Norvig在他的书中制作了一个Prolog解释器。该书链接为:http://norvig.com/paip.html,Prolog解释器链接为:http://norvig.com/paip/prolog.lisp。 - Özgür
3
注意,Lisp是面向对象的。请查看Common Lisp Object System。 - catphive
有一些宏可以让你直接在Lisp中编写C代码,但是C代码可以被强大的Lisp语言遍历,而宏可以在编译时任意运行代码,而不是运行时。 - aoeu256
14个回答

36

这是一个非常好的问题。

我认为这个问题很微妙,但绝对可以回答:

宏并不固定在s表达式中。LOOP宏就是用关键字(符号)编写的非常复杂的语言。因此,虽然你可能会在循环的开头和结尾使用括号,但里面有自己的语法。

例如:

(loop for x from 0 below 100
      when (even x)
      collect x)

话虽如此,大多数简单的宏只使用s表达式。并且你会“被困”在它们中使用。

但是就像Sergio所回答的那样,s表达式开始感觉正确。语法脱离了束缚,你开始在语法树上编码。

至于阅读器宏,是的,你可以构想出这样的代码:

#R{
      ruby.code.goes.here
  }

但你需要编写自己的Ruby语法解析器。

你还可以使用宏来模仿一些Ruby结构,例如块,这些宏会编译成现有的Lisp结构。

#B(some lisp (code goes here))

会被翻译成

(lambda () (some lisp (code goes here)))

参见此页面了解如何执行。


1
你的宏“不卡在s表达式中”的例子在我看来看起来像一个s表达式。 - Hugh Allen
13
循环宏示例是一个s表达式,但是“子表达式”不是。 - Eric Normand
这是一个非常好的问题。但对于stackoverflow.com来说并不适用;它不是一个可以明确回答的任务,也无法避免意见和宗教在分析停滞不前的地方填补空白的情况。 - Kaz
2
@Kaz,但不知怎么回事,这并没有发生,答案也很棒 :) - Luka Ramishvili

24

是的,您可以重新定义语法,使Lisp成为编译器。您可以使用“读取器宏”来实现这一点,这与您可能想到的普通“编译器宏”不同。

Common Lisp具有内置的功能,用于定义阅读器的新语法和处理该语法的阅读器宏。此处理在读取时间完成(在编译或评估时间之前)。要了解有关在Common Lisp中定义阅读器宏的更多信息,请参见Common Lisp Hyperspec--您将需要阅读第2章,“语法”第23章,“阅读器”。(我相信Scheme也具有相同的功能,但我对它不太熟悉--请参见Scheme sources以获取Arc编程语言)。

作为一个简单的例子,假设您希望Lisp使用花括号而不是圆括号。这需要类似于以下阅读器定义:

;; { and } become list delimiters, along with ( and ).
(set-syntax-from-char #\{ #\( )
(defun lcurly-brace-reader (stream inchar) ; this was way too easy to do.
  (declare (ignore inchar))
  (read-delimited-list #\} stream t))
(set-macro-character #\{ #'lcurly-brace-reader)

(set-macro-character #\} (get-macro-character #\) ))
(set-syntax-from-char #\} #\) )

;; un-lisp -- make parens meaningless
(set-syntax-from-char #\) #\] ) ; ( and ) become normal braces
(set-syntax-from-char #\( #\[ )

你告诉Lisp,{就像(,}就像)。然后你创建一个函数(lcurly-brace-reader),当读取器看到{时会调用这个函数,并使用set-macro-character将该函数分配给{。然后你告诉Lisp,(和)就像[和]一样(也就是说,不是有意义的语法)。
其他你可以做的事情包括,例如创建新的字符串语法或使用[和]来包含中缀符号并将其处理成S表达式。
你甚至可以远远超越这个范围,重新定义整个语法,使用自己的宏字符触发读取器中的操作,因此天空真的是极限。这就是为什么Paul Graham其他人一直说Lisp是编写编译器的好语言之一的原因之一。

16

我并不是Lisp专家,甚至不是Lisp程序员,但在尝试该语言后,我得出结论:使用一段时间后,括号开始变得“无形”,你开始把代码看成自己想要的样子。你开始更加关注通过s-exprs和宏创建的句法结构,而不是列表和圆括号的词汇形式。

如果你利用一个帮助缩进和语法着色的好编辑器(尝试将括号设置为与背景非常相似的颜色),这一点尤其正确。

你可能无法完全替换语言并获得“Ruby”语法,但你并不需要它。由于语言的灵活性,如果你想,你可以最终拥有一种感觉像你正在遵循“Ruby编程风格”的方言,无论你对此的理解是什么。

我知道这只是一种经验观察,但我认为当我意识到这一点时,我曾经有过Lisp启示时刻。


15
一遍又一遍地,Lisp的新手们都想要“摆脱所有的括号”。 这种情况持续了几周。 试图在通常的S表达式解析器上构建一个严肃的通用编程语法项目从未成功过,因为程序员总是最终更喜欢当前被认为是“括号地狱”的状态。需要一点时间适应,但并不需要太多!一旦你适应了它,你就能真正欣赏默认语法的可塑性,回到只有一种方式表达任何特定编程结构的语言中会让人感到不舒服。
话虽如此,Lisp是构建特定领域语言的优秀基础。与XML一样好,甚至更好。
祝你好运!

我承认我从未长时间使用过Lisp或其方言,但是我非常习惯使用它的语法(其简洁性相当棒),但我仍然更喜欢Ruby的语法,胜过我使用过的任何其他语言。 - Mike Stone
2
如果语法可以是你认为适合表达手头程序的任何形式,那该怎么办? - jfm3
1
实际上,有人在S表达式的基础上开发了一个严肃的通用编程语法 - "Sweet-expressions"。它们保留了S表达式的所有灵活性优势。那个扩展的Lisp熟悉用户喜欢"括号地狱"。然而,Sweet-expressions并没有流行起来。我假设这是因为Sweet-expressions的作者仅为少数Lisp方言实现了该语法,并且对如何安装这些扩展编写了很少的文档。 - Rory O'Kane

14

我见过的关于Lisp宏的最好解释在这里,讲者是Peter Seibel,他是《Practical Common Lisp》一书的作者,也是最好的Lisp教材。

通常很难解释使用Lisp宏的动机,因为它们真正发挥作用的情况太复杂,无法在简单的教程中呈现。但是Peter提供了一个很好的例子,你可以完全理解它,并且它充分、适当地使用了Lisp宏。

你问:“你能否将LISP的函数性质转换为更具面向对象的语法和语义。” 答案是肯定的。实际上,Lisp最初根本没有任何面向对象的编程,这并不奇怪,因为Lisp早在面向对象编程之前就已存在!但是,当我们在1978年第一次了解面向对象编程时,我们能够轻松地将其添加到Lisp中,其中包括使用宏等。最终开发出了Common Lisp Object System(CLOS),这是一个非常强大的面向对象编程系统,可以优雅地适应Lisp。整个系统可以作为扩展加载-- 没有任何内置功能!所有功能都是由宏实现的。

Lisp还具有一种完全不同的功能,称为“读取器宏”,可用于扩展语言的表面语法。使用读取器宏,您可以创建具有类似C或Ruby语法的子语言。它们在内部将文本转换为Lisp。大多数真正的Lisp程序员并不广泛使用这些宏,主要是因为很难扩展交互式开发环境以理解新语法。例如,Emacs缩进命令可能会被新语法所困惑。但是,如果你精力充沛,那么Emacs也是可扩展的,你可以教它理解你的新词法语法。


11
括号地狱?在这里我看不到更多的括号:
(function toto)

比在:

function(toto);

而且在

(if tata (toto)
  (titi)
  (tutu))

不超过:

if (tata)
  toto();
else
{
  titi();
  tutu();
}

我看到括号和分号用得少了。


1
看看一些 Ruby 语法... 除非绝对需要消除歧义,否则可以完全省略括号... 即使是强函数式的 Haskell 也不像 Lisp(或其他语言,如你所指出的)那样受到括号的限制。 - Mike Stone
1
@Mike:你说得对。但是Lisp与C / C ++ / Java / C#相比:它没有那么多括号,而且括号少得多,也没有分号!而这是50年前的事情,这总是让我感到惊讶。 - Sébastien RoccaSerra
可以用Lisp编写一个阅读器,它使用缩进而不需要括号。这大约需要一页的代码。那会改变多少呢?这就像为了外表找妻子一样...尽管这很愚蠢,但很多人都这样做... :) - Attila Lendvai
Attila,每当我看到Python代码时,我就会想是否可以编写一个不需要特定空格并使用括号的读取器。 - Svante

11

常规宏操作列表对象。通常,这些对象是其他列表(因此形成树形结构)和符号,但它们也可以是其他对象,例如字符串、哈希表、用户定义的对象等。这些结构称为s-exps

因此,当您加载源文件时,Lisp编译器将解析文本并生成s-exps。宏在这些上操作。这非常有效,并且是一种神奇的方式,在s-exps的精神内扩展语言。

此外,通过“读取器宏”,上述解析过程可以扩展,该宏允许您自定义编译器将文本转换为s-exps的方式。但我建议您接受Lisp的语法,而不是将其弯曲成其他东西。

当您提到Lisp的“功能性质”和Ruby的“面向对象语法”时,您似乎有点困惑。我不确定“面向对象语法”应该是什么,但Lisp是一种多范式语言,极其支持面向对象编程。

顺便说一下,当我说Lisp时,我指的是Common Lisp

我建议你放下偏见,认真试一试Lisp


9
你所问的有点像是想成为一名专业的巧克力师傅,以便你可以从你最喜欢的巧克力蛋糕中去除那些可恶的棕色物质。

6

是的,你可以从根本上改变语法,甚至避免“括号地狱”。为此,你需要定义一个新的读取器语法。请查看读取器宏。

然而,我怀疑要达到编写这些宏的Lisp专业水平,你需要深入了解这种语言,以至于你不再认为括号是“地狱”。也就是说,当你知道如何避免它们时,你会开始接受它们作为一件好事。


2
如果您希望Lisp看起来像Ruby,请使用Ruby。
可以以非常Lisp的方式使用Ruby(和Python),这是它们迅速获得认可的主要原因之一。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接