是否有任何允许语法抽象的非Lisp方言?

Mik*_*ike 12 lisp language-features functional-programming abstract-syntax-tree

正如Rich Hickey所说,Lisp语言的秘诀在于能够通过宏直接操作抽象语法树.这可以用任何非Lisp方言语言实现吗?

Eli*_*lay 22

能够"直接操纵抽象语法树"本身并不是什么新鲜事,尽管这是很少有语言的东西.例如,现在许多语言都有某种eval功能 - 但很明显,这不是操纵抽象语法树,而是操纵具体语法 - 直接源代码.顺便提一下,D中提到的功能与CPP属于同一类别:两者都处理原始源文本.

举一个具有该功能的语言示例(但不是适当的宏),请参阅OCaml.它有一个语法扩展系统CamlP4,它本质上是一个编译器扩展工具包,它围绕OCaml抽象语法作为其最重要的目的.但这仍然不是Lisps中相应功能如此之大的原因.

Lisps的重要特性是,使用宏获得的扩展是语言的一部分,与任何其他语法形式相同.换句话说,当你使用像ifLisp 这样的东西时,无论是作为宏还是作为原始形式实现,功能上都没有区别.(实际上存在一些细微的差别:在某些情况下,了解一组不进一步扩展的原始形式非常重要.)更具体地说,Lisp库可以提供普通的绑定宏,这意味着库可以扩展语言比大多数语言中常见的无聊扩展更有趣的方式,只能添加普通绑定(函数和值).

现在,从这个角度来看,像D设施这样的东西在性质上非常相似.但它处理原始文本而不是AST的事实限制了它的实用性.如果你看一下该页面上的例子,

mixin(GenStruct!("Foo", "bar"));
Run Code Online (Sandbox Code Playgroud)

你可以看到这看起来不像是语言的一部分 - 为了让它更像Lisp,你可以用自然的方式使用它:

GenStruct(Foo, bar);
Run Code Online (Sandbox Code Playgroud)

不需要mixin标记使用宏的关键字,不需要!,标识符被指定为标识符而不是字符串.更好的是,定义应该更自然地表达,例如(在这里发明一些错误的语法):

template expression GenStruct(identifier Name, identifier M1) {
    return [[struct $Name$ { int $M1$; }; ]]
}
Run Code Online (Sandbox Code Playgroud)

这里需要注意的一件重要事情是,由于D是一种静态类型语言,因此AST已经以明确的方式进入这种心理练习 - 作为identifierexpression类型(我在这里假设它template将此标记为宏定义,但它仍然需要返回类型).

在Lisp中,你实际上得到的东西非常接近这个功能,而不是糟糕的字符串解决方案.但是你得到的更多--Lisp故意在基本列表类型上发挥作用,并以一种非常简单的方式将AST与运行时语言统一起来:AST由符号和列表以及其他基本文字(数字,字符串,布尔值)组成,这些都是运行时语言的一部分.事实上,对于那些文字,Lisp向前迈出了一步,并使用文字作为自己的语法 - 例如,数字123(运行时存在的值)由语法表示,也是数字123(但现在它是编译时存在的值).最重要的是,与其他语言称之为"宏"相比,Lisp中与宏相关的代码往往更容易处理.想象一下,例如,让D示例代码在结构中创建N个 int字段(其中N是宏的新输入) - 这需要使用一些函数将字符串转换为数字.


Mer*_*ham 6

口齿不清

LISP"特殊"的原因是......

内置功能非常经济:

  • 唯一的内置数据结构是原子或列表
  • 语法是根据列表数据结构实现的
  • "系统功能"很少

它支持函数,使新函数定义与内置函数无法区分:

  • 调用语法是相同的
  • 论证的评估可以完全控制

它支持宏,以便始终可以根据特定于域的语言定义任意Lisp代码:

  • 调用语法就像自定义函数调用语法,就像内置函数调用语法一样
  • 论证的评估是完全可控的
  • 任意Lisp代码生成都是可能的
  • 宏在运行时进行评估,因此宏的实现可以在生成新代码时调用现有代码

有了上述功能,您可以:

  • 用很少的代码重新实现Lisp-in-Lisp
  • 以与内置功能无法区分的方式添加任何现有的编程习语

例如,您可以轻松地在Lisp之上实现名称空间,任何数据结构,类,多态和多分派系统,并且这些功能将像Lisp中内置的那样工作.

其他语言

但这一切都取决于你的定义.其他语言以不同的方式支持某些级别的"句法抽象".其中一些方法比其他方式更强大,几乎与Lisp的灵活性相匹配.

一些例子:

在Boo中,您可以使用语法宏来定义将由编译器自动处理的新DSL.有了这个,您可以在现有功能的基础上实现任何语言功能.与Lisp相比,这些限制是在编译时对它们进行评估,因此不直接支持运行时代码生成.

在Javascript中,数据结构是通用且灵活的(一切都是内置类型或关联数组).它还支持直接从关联数组调用函数.有了这个,您可以在现有功能(如类和命名空间)之上实现多种语言功能.

因为Javascript是一种动态语言(函数调用的名称是在运行时评估的),并且因为它在数据结构的上下文中公开了内置特性,所以它完全是"反射的"并且完全可变.

因此,您可以使用自己的功能替换或填充现有系统功能.这通常在填充您自己的运行时调试功能或沙箱中非常有用(通过取消定义系统调用,您不希望隔离代码访问).

在大多数这些方面,Lua与Javascript非常相似.

C++预处理器允许您使用与现有函数调用类似的语法来定义自己的DSL.它不会让你控制评估(这是许多错误的来源,以及大多数人说的原因C/C++ macros are "Evil"),但它确实支持一种有限形式的代码生成.

C/C++宏中的代码生成支持是有限的,因为在编译代码之前会对宏进行求值,并且无法通过C代码进行控制.它几乎完全局限于文本替换.这极大地限制了可以生成的代码类型.

C++模板功能非常强大(WRT到C/C++宏),用于语言的语法添加.它可以将大量运行时代码评估转换为编译时代码评估,并且可以对现有代码执行静态断言.它可以以有限的方式引用现有的C++代码.

但模板元编程(TMP)非常难以处理,因为它具有可怕的语法,是C++的一个非常严格限制的子集,具有非常有限的代码生成能力,并且无法在运行时进行评估.C++模板也可以输出您在编程时遇到的最难的错误消息:)

请注意,这并未使模板元编程成为许多社区中活跃的研究领域.请参阅boost项目,其中很大一部分专门用于TMP支持库和TMP实现的库.

Duck输入可以允许您在对象上定义语法,以便在运行时替换实现.这类似于Javascript在关联数组上定义函数的方式.

我不能说Python(因为我不太清楚),但鸭子打字通常比Javascript的动态功能更有限,因为缺乏反射性,可变性以及通过可反射/可变接口暴露系统功能.例如,C#的鸭子打字在所有这些方面都受到限制.


SK-*_*gic 6

为了完整起见,除了已经提到的语言和预处理器之外: