具有类似Lisp的语法扩展机制的编程语言


20

我对Lisp的了解有限(尝试在业余时间学习一些知识),但据我了解,Lisp宏允许通过在Lisp本身中进行描述来引入新的语言结构和语法。这意味着可以将新构造作为库添加,而无需更改Lisp编译器/解释器。

这种方法与其他编程语言完全不同。例如,如果我想用一种新的循环或某种特定的习惯来扩展Pascal,则必须扩展该语言的语法和语义,然后在编译器中实现该新功能。

Lisp家族之外是否还有其他编程语言(即Common Lisp,Scheme,Clojure(?),Racket(?)等)提供了类似的在语言本身内扩展语言的可能性?

编辑

请避免进一步讨论,并在回答中要具体。我希望从概念的角度来理解一堆专门针对Lisp宏作为扩展机制的东西,以及哪些非Lisp编程语言可以提供某种概念,而不是一堆可以以某种方式进行扩展的编程语言的清单。靠近他们。


6
Lisp除了常规宏之外还有另一个技巧。“阅读器宏”允许在运行时捕获并扩展解析器语法,因此即使该语言的基本标记结构也处于控制之下。
ddyer 2012年

@ddyer:谢谢,我不知道这件事:另一个要添加到我的阅读列表中的主题。
Giorgio 2012年

我敢打赌Ruby元编程可以满足此要求。
钻机2012年

1
您的问题是矛盾的,首先要索要清单,然后索要概念性信息,是吗???

我要的是列表,而不是通用列表(不只是可以以某种方式扩展的任何编程语言),因为这太笼统了。我想知道可以以类似于Lisp的方式扩展的语言(扩展使用与扩展相同的语言定义,而不是某种元语言)。PéterTörök,Thomas Eding,SK-logic和Mechanical snail的答案似乎与我的想法最为接近。我仍然必须仔细阅读所有答案。
乔治

Answers:


19

Scala也使之成为可能(事实上,它是有意识地设计来支持新语言结构甚至完整DSL的定义)。

除了功能语言中常见的高阶函数,lambda和curring外,这里还有一些特殊的语言功能可启用此功能*:

  • 没有运算符-一切都是函数,但是函数名可以包含特殊字符,例如'+','-'或':'
  • 单参数方法调用可以省略点和花括号,即a.and(b)等价于a and b中缀形式
  • 对于单参数函数调用,您可以使用大括号而不是普通的花括号-这(与currying一起)允许您编写类似

    val file = new File("example.txt")
    
    withPrintWriter(file) {
      writer => writer.println("this line is a function call parameter")
    }
    

    其中withPrintWriter是具有两个参数列表的简单方法,两个参数列表都包含一个参数

  • 按名称的参数允许您省略lambda中的空参数列表,从而使您可以将调用写成以下myAssert(() => x > 3)形式:myAssert(x > 3)

免费的《编程Scala》一书的第11章详细讨论了示例DSL的创建。

* 我并不是说这些是Scala所独有的,但至少它们似乎并不十分常见。我不是功能语言方面的专家。


1
+1:有趣。这是否意味着为了扩展语法,我可以定义新的类,并且方法签名会将新语法作为副产品提供?
Giorgio 2012年

@乔治,基本上是。
彼得Török

链接不再起作用。
内特·格伦

13

Perl允许对其语言进行预处理。虽然这在某种程度上不经常用于根本地改变语言语法的程度,但可以在某些...奇怪的模块中看到它:

还有一个模块,允许perl运行看起来像用Python编写的代码。

在perl中对此更现代的方法是使用Filter :: Simple(perl5中的核心模块之一)。

请注意,所有这些示例都涉及Damian Conway,他被称为“ Perl疯狂医生”。在perl中,它仍然具有惊人的强大功能,可以根据需要改变语言。

perlfilter上有更多有关此方法和其他替代方法的文档


13

哈斯克尔

Haskell具有“模板Haskell”和“准引用”:

http://www.haskell.org/haskellwiki/Template_Haskell

http://www.haskell.org/haskellwiki/Quasiquotation

这些功能使用户可以在正常方式之外大幅增加该语言的语法。这些也可以在编译时解决,我认为这是必须要做的(至少对于编译语言而言)[1]。

之前我曾在Haskell中使用准引号在类似C的语言上创建高级模式匹配器:

moveSegment :: [Token] -> Maybe (SegPath, SegPath, [Token])
moveSegment [hc| HC_Move_Segment(@s, @s); | s1 s2 ts |] = Just (mkPath s1, mkPath s2, ts)
moveSegment _ = Nothing

[1]另外,以下内容也可以用作语法扩展:runFeature "some complicated grammar enclosed in a string to be evaluated at runtime",这当然是胡扯。


3
Haskell还具有其他功能,这些功能本质上允许使用自定义语法,例如创建自己的运算符或与lambda函数耦合的自动curring(请考虑的典型用法forM)。
Xion 2012年

老实说,我认为currying和自定义运算符没有资格。它们允许人们整齐地使用该语言,但不允许您向该语言添加/ new /功能。TH和QQ。从严格的意义上讲,TH和QQ都不能完全按照其设计的目的进行操作,但是它们允许您在编译时真正脱离语言。
Thomas Eding 2012年

1
“否则,以下内容可作为语法扩展...”:非常好。
乔治

12

Tcl具有支持可扩展语法的悠久历史。例如,这是一个循环的实现,该循环在基数,它们的平方和它们的立方体上迭代三个变量(直到停止):

proc loopCard23 {cardinalVar squareVar cubeVar body} {
    upvar 1 $cardinalVar cardinal $squareVar square $cubeVar cube

    # We borrow a 'for' loop for the implementation...
    for {set cardinal 0} true {incr cardinal} {
        set square [expr {$cardinal ** 2}]
        set cube [expr {$cardinal ** 3}]

        uplevel 1 $body
    }
}

然后将这样使用:

loopCard23 a b c {
    puts "got triplet: $a, $b, $c"
    if {$c > 400} {
        break
    }
}

这种技术广泛用于Tcl编程中,合理地使用它的关键是upvarand uplevel命令(upvar将另一个作用域中的已命名变量绑定到局部变量,并uplevel在另一个作用域中运行脚本:在两种情况下,1表示有问题的范围是调用方的)。它还在与数据库耦合的代码中使用了很多代码(为结果集中的每一行运行一些代码),在GUI的Tk中使用了代码(用于将回调绑定到事件),等等。

但是,这只是完成的一小部分。嵌入式语言甚至不需要是Tcl。它几乎可以是任何东西(只要它平衡了花括号,如果语法不正确,那么语法就会变得很恐怖,这是绝大多数程序),Tcl可以根据需要将其分发给嵌入式外语。这样做的示例包括嵌入C以实现Tcl命令以及与Fortran等效的命令。(可以说,Tcl的所有内置命令在某种意义上都是以这种方式完成的,因为它们实际上只是一个标准库,而不是语言本身。)


10

这部分是语义问题。Lisp的基本思想是程序是可以自身进行操作的数据。Lisp系列中的常用语言(例如Scheme)实际上并不能让您在解析器意义上添加新的语法。都是用空格分隔的列表。只是因为核心语法​​只做很少的事情,所以您几乎可以使用它来进行任何语义构造。Scala(在下面讨论)是相似的:变量名规则是如此自由,以至于您可以轻松地用它来制作漂亮的DSL(同时保持在相同的核心语法规则之内)。

这些语言虽然实际上并不能让您从Perl过滤器的角度定义新语法,但它们具有足够灵活的内核,您可以使用它来构建DSL和添加语言构造。

重要的共同特征是,它们使您可以使用语言公开的功能来定义既可以使用又可以内置的语言构造。此功能的支持程度有所不同:

  • 许多老的语文提供内置的功能一样sin()round()等等,没有任何方式来实现自己的。
  • C ++提供了有限的支持。例如一些内置的样管型关键字(static_cast<target_type>(input)dynamic_cast<>()const_cast<>()reinterpret_cast<>())可以使用模板功能,升压用途仿真lexical_cast<>()polymorphic_cast<>()any_cast<>(),...
  • Java已经内置了控制结构(for(;;){}while(){}if(){}else{}do{}while()synchronized(){}strictfp{}),并不会让你定义自己。相反,Scala定义了一种抽象语法,该语法使您可以使用类似于控件结构的便利语法来调用函数,而库使用此语法来有效地定义新的控件结构(例如react{},在actors库中)。

另外,您可能会在Notation包中查看Mathematica的自定义语法功能。(从技术上讲,它属于Lisp家族,但是具有一些可扩展性功能,与通常的Lisp可扩展性不同。)


2
错误。Lisp语言实际上允许您添加绝对任何一种新的语法。就像这个例子:meta-alternative.net/pfront.pdf-它只是Lisp宏。
SK-logic

这似乎是以专门为构建DSL设计的语言实现的。当然,您可以在Lisp系列中创建一种语言,该语言也提供此类功能。我的意思是,该功能不是常用的Lisps(例如Scheme)支持的Lisp核心思想。编辑以澄清。
机械蜗牛

这种“用于构建DSL的语言”是在非常典型的,简约的Lisp之上构建的宏的集合。它可以很容易地移植到任何其他Lisp (defmacro ...)。实际上,我目前正在将该语言移植到Racket,只是为了好玩。但是,我同意这不是很有用,因为对于大多数可能有用的语义而言,S表达式语法已绰绰有余。
SK-logic

而且,Scheme与Common Lisp并无任何不同,从R6RS正式开始,并且非正式地存在了很长一段时间,因为它确实提供了(define-macro ...)等效的功能,从而可以在内部使用任何类型的解析。
SK-logic

8

Rebol的声音听起来几乎与您所描述的相似,但有些偏斜。

除了定义特定的语法外,Rebol中的所有内容都是函数调用-没有关键字。(是的,您可以重新定义ifwhile如果您确实愿意的话)。例如,这是一条if语句:

if now/time < 12:00 [print "Morning"]

if是一个带有2个参数的函数:一个条件和一个块。如果条件为真,则对块进行评估。听起来像大多数语言,对不对?好吧,该块是数据结构,不限于代码-例如,这是一个块块,也是“代码就是数据”灵活性的快速示例:

SomeArray: [ [foo "One"] [bar "Two"] [baz "Three"] ]
foreach action SomeArray [action/1: 'print] ; Change the data
if now/time < 12:00 SomeArray/2 ; Use the data as code - right now, if now/time < 12:00 [print "Two"]

只要您能够遵守语法规则,那么在大多数情况下,扩展这种语言就是定义新功能。例如,某些用户已将Rebol 3的功能反向移植到Rebol 2中。


7

Ruby具有相当灵活的语法,我认为这是“在语言本身内部扩展语言”的一种方式。

一个例子是rake。它是用Ruby编写的,它是Ruby,但看起来像make

要检查某些可能性,可以查找关键字Rubymetaprogramming


13
“灵活语法”与“可扩展语法”非常不同。自从我用Ruby编程以来已经有很长时间了,但是Rake看起来像是对内置语法的精心设计。换句话说,这不是示例。
Thomas Eding 2012年

2
这不是程度问题,不是种类问题吗?您如何区分可以扩展其语法某些方面但不能扩展其他方面的“可扩展语法”语言与“灵活语法”语言?
即将到来的暴风雨2012年

1
如果该行模糊,那么让我们往回推一下,以便C被认为支持可扩展语法。
Thomas Eding 2012年

为了与您的示例链接,我将在此处划清界线:一种具有可扩展语法的语言可以使rake看起来像make。可以扩展具有灵活语法的语言(在那里,可以进行很好的语言混合)来编译和运行makefile。您关于程度的观点虽然很好。也许某些语言允许编译Make,但Python不行。其他人将两者都考虑在内。
克莱顿·斯坦利

应该可以使用任何图灵完整的语言来处理Make文件。语法的灵活性并不是挑战的多大因素。
钻机2012年

7

以您正在谈论的方式扩展语法,可以创建特定域的语言。 因此,重述您的问题最有用的方法是,还有哪些其他语言对特定领域的语言有很好的支持?

Ruby具有非常灵活的语法,并且许多DSL都在这里蓬勃发展,例如rake。Groovy包含很多优点。它还包括AST转换,它与Lisp宏更直接相似。

R是用于统计计算的语言,它允许函数对其参数进行不评估。它使用它来创建用于指定回归公式的DSL。例如:

y ~ a + b

表示“将形式为k0 + k1 * a + k2 * b的线拟合到y中的值”。

y ~ a * b

表示“将形式为k0 + k1 * a + k2 * b + k3 * a * b的线拟合到y中的值”。

等等。


2
与Lisp或Clojure宏相比,Groovy的AST转换非常冗长。例如,groovy.codehaus.org / Global + AST + Transformations上的20+行Groovy示例可以重写为Clojure中的短行,例如`(this(println〜message))。不仅如此,而且您也不必在该Groovy页面上编译jar,编写元数据或任何其他内容。
Vorg van Geir 2012年

7

融合是另一种非敏捷的元编程语言。并且,在某种程度上,C ++也符合条件。

可以说,MetaOCaml与Lisp相距甚远。要获得完全不同的语法可扩展性样式,但又非常强大,请看一下CamlP4

Nemerle是另一种具有Lisp风格的元编程的可扩展语言,尽管它更接近于Scala之类的语言。

而且,Scala本身也将很快成为这种语言。

编辑:我忘记了最有趣的示例-JetBrains MPS。它不仅与任何Lispish都相距甚远,它甚至是一个非文本编程系统,其编辑器直接在AST级别上运行。

Edit2:要回答一个更新的问题-Lisp宏中没有什么独特之处。从理论上讲,任何语言都可以提供这样的机制(我什至使用纯C语言也做到了)。您所需要做的就是访问AST,并能够在编译时执行代码。一些反思可能会有所帮助(查询类型,现有定义等)。


您的Scala链接明确表示建议的宏“无法更改Scala的语法”。(有趣的是,它将此列为该提案和C / C ++预处理程序宏之间的区别!)
ruakh 2012年

@ruakh,是的,它与Converge和Template Haskell的方法相同-宏应用程序已显式标记,不能与“常规”语法混合使用。但是,在内部您可以使用任何喜欢的语法,因此可以说它是可扩展的语法。不幸的是,“ non-lisp”要求将您的选择限制为此类语言。
SK-logic

“我什至用纯C语言也做到了”:这怎么可能?
乔治

@Giorgio,我当然已经修改了编译器(添加了宏和增量模块编译,这对于C语言来说实际上是很自然的)。
SK-logic

为什么需要访问AST?
Elliot Gorokhovsky

6

Prolog允许定义新的运算符,这些运算符将转换为相同名称的复合词。例如,这定义了一个has_cat运算符并将其定义为检查列表是否包含原子的谓词cat

:- op(500, xf, has_cat).
X has_cat :- member(cat, X).

?- [apple, cat, orange] has_cat.
true ;
false.

xf该装置has_cat是一个后缀符; 使用fx将使它成为前缀运算符,并xfx使其成为中缀运算符,并带有两个参数。检查此链接以获取有关在Prolog中定义运算符的更多详细信息。


5

TeX完全不在列表中。你们都知道吧?看起来像这样:

Some {\it ``interesting''} example.

…除了可以无限制地重新定义语法。可以为语言中的每个(!)标记赋予新的含义。ConTeXt是一个宏程序包,它用方括号替换了花括号:

Some \it[``interesting''] example.

更常见的宏程序包LaTeX也为此重新定义了语言,例如添加了\begin{environment}…\end{environment}语法。

但这并不止于此。从技术上讲,您也可以重新定义令牌以解析以下内容:

Some <it>“interesting”</it> example.

是的,绝对有可能。一些软件包使用它来定义特定于域的小型语言。例如,TikZ软件包为技术图纸定义了简洁的语法,它允许以下操作:

\foreach \angle in {0, 30, ..., 330} 
  \draw[line width=1pt] (\angle:0.82cm) -- (\angle:1cm);

此外,TeX已经完成了Turing,因此您可以使用它完成所有工作。我从未见过这种方法可以发挥其全部潜能,因为它毫无意义且非常复杂,但是完全可以通过重新定义令牌来使以下代码可解析(但这可能会达到解析器的物理极限,因为的构建方式):

for word in [Some interesting example.]:
    if word == interesting:
        it(word)
    else:
        word

5

Boo让您可以在编译时通过语法宏大量自定义语言。

Boo有一个“可扩展的编译器管道”。这意味着编译器可以在编译器管道的任何时候调用您的代码进行AST转换。如您所知,诸如Java的Generics或C#的Linq之类的东西在编译时只是语法转换,因此它非常强大。

与Lisp相比,主要优点是它可以与任何语法一起使用。Boo使用的是Python启发式的语法,但是您可能会编写使用C或Pascal语法的可扩展编译器。并且由于宏是在编译时评估的,因此不会对性能造成任何影响。

与Lisp相比,缺点是:

  • 使用AST不如使用S表达式好用
  • 由于宏是在编译时调用的,因此它无权访问运行时数据。

例如,这就是您可以实现新控件结构的方式:

macro repeatLines(repeatCount as int, lines as string*):
    for line in lines:
        yield [| print $line * $repeatCount |]

用法:

repeatLines 2, "foo", "bar"

然后在编译时将其转换为以下内容:

print "foo" * 2
print "bar" * 2

(不幸的是,Boo的在线文档总是毫无希望地过时,甚至没有涵盖诸如此类的高级内容。我所知道的语言的最佳文档是本书:http : //www.manning.com/rahien/


1
目前此功能的网络文档已损坏,我自己也不写Boo,但我认为如果忽略它会很可惜。我很欣赏mod的反馈,并将重新考虑我如何在业余时间贡献免费信息。
2012年

4

Mathematica评估基于模式匹配和替换。这样,您就可以创建自己的控件结构,更改现有控件结构或更改表达式的求值方式。例如,您可以这样实现“模糊逻辑”(稍微简化一下):

fuzzy[a_ && b_]      := Min[fuzzy[a], fuzzy[b]]
fuzzy[a_ || b_]      := Max[fuzzy[a], fuzzy[b]]
fuzzy[!a_]           := 1-fuzzy[a]
If[fuzzy[a_], b_,c_] := fuzzy[a] * fuzzy[b] + fuzzy[!a] * fuzzy[c]

这将覆盖对预定义逻辑运算符&&,|| ,!的求值。和内置If子句。

您可以像函数定义一样阅读这些定义,但真正的含义是:如果表达式与左侧描述的模式匹配,则将其替换为右侧的表达式。您可以这样定义自己的If子句:

myIf[True, then_, else_] := then
myIf[False, then_, else_] := else
SetAttributes[myIf, HoldRest]

SetAttributes[..., HoldRest] 告诉评估者它应该在模式匹配之前对第一个参数求值,而对其余参数保持评估,直到模式被匹配和替换之后。

它在Mathematica标准库中广泛使用,例如,定义一个D接受表达式并求值为其符号导数的函数。


3

Metalua是提供此功能的语言和与Lua兼容的编译器。

  • 与Lua 5.1源代码和字节码完全兼容:简洁,优雅的语义和语法,惊人的表达能力,良好的性能,几乎通用的可移植性。 -完整的宏系统,功能类似于Lisp方言或Template Haskell所提供的功能;被操纵的程序可以被视为
    源代码,抽象语法树或它们的任意组合
    ,以更适合您的任务的形式出现。
  • 动态可扩展的解析器,可让您使用与其他语言完美融合的语法来支持宏。

  • 一组语言扩展,全部作为常规的metatu宏实现。

与Lisp的差异:

  • 当开发人员不编写宏时,不要为开发人员感到烦恼:在我们不编写宏的95%的时间内,该语言的语法和语义应该最适合。
  • 鼓励开发人员遵循该语言的约定:不仅使“最佳实践”无人听,而且通过提供一种API,该API使Metalua Way的编写变得更加容易。开发人员的可读性比编译器的可读性更重要,更难实现,为此,拥有一套通用的受尊重的约定将大有帮助。
  • 尽力而为。Lua和Metalua都没有受到强制性束缚和纪律约束,因此,如果您知道自己在做什么,该语言将不会妨碍您。
  • 当发生一些有趣的事情时使它变得显而易见:所有元操作都发生在+ {...}和-{...}之间,并且在视觉上不遵守常规代码。

应用程序的一个示例是ML类模式匹配的实现。

另请参阅:http : //lua-users.org/wiki/MetaLua


尽管Lua确实不是Lisp,但是您的“差异”列表非常可疑(唯一相关的项目是最后一个)。而且,当然,Lisp社区不会同意不应该在95%的时间内编写/使用宏的说法-Lisp方式倾向于在宏的基础上,在95%的时间内使用和编写DSL来编写/使用宏。课程。
SK-logic

2

如果您正在寻找可扩展的语言,则应该看看Smalltalk。

在Smalltalk中,编程的唯一方法是实际扩展语言。IDE,库或语言本身之间没有区别。它们是如此交织在一起,以至于Smalltalk通常被称为环境而不是语言。

您没有在Smalltalk中编写独立的应用程序,而是扩展了语言环境。

请访问http://www.world.st/以获取少量资源和信息。

我想推荐Pharo作为进入Smalltalk世界的方言:http : //pharo-project.org

希望能有所帮助!


1
听起来不错。
Thomas Eding 2012年

1

有一些工具可以制作自定义语言,而无需从头开始编写整个编译器。例如,有一个Spoofax,它是代码转换工具:您输入了输入语法和转换规则(以非常高级的声明性方式编写),然后可以生成Java源代码(或其他语言,如果您足够关心的话)。来自您设计的自定义语言。

因此,可以采用语言X的语法,定义语言X'(具有自定义扩展名的X)的语法并转换X'→X,Spoofax将生成编译器X'→X。

目前,如果我理解正确,最好的支持是Java,而C#支持正在开发中(或者我听说)。但是,该技术可以应用于具有静态语法的任何语言(例如,可能不是Perl)。


1

Forth是另一种高度可扩展的语言。许多Forth实现都是由用汇编程序或C编写的小内核组成的,然后其余语言由Forth本身编写。

还有一些受Forth启发并共享此功能的基于堆栈的语言,例如Factor


0

Funge-98

Funge-98的指纹功能允许完成该语言的整个语法和语义的完全重组。但是只有实现者提供了允许用户以编程方式更改语言的指纹机制(理论上可以在常规Funge-98语法和语义中实现)。如果是这样,可以从字面上使文件的其余部分(或文件的任何部分)充当C ++或Lisp(或他想要的任何东西)。

http://quadium.net/funge/spec98.html#Fingerprints


您为什么单独发布此帖子,而不添加到以前的答案中
gnat 2012年

1
因为Funge与Haskell无关。
Thomas Eding 2012年

-1

要获得所需的内容,您确实需要那些括号和语法的缺乏。几种基于语法的语言可能接近,但这与真正的宏并不完全相同。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.