【译文】Z:一种基于Z-表达式的新语言

2013-01-04 00:00

【译文】Z:一种基于Z-表达式的新语言

by

at 2013-01-03 16:00:00

original http://www.soimort.org/posts/132

Original Article: A tiny language called Z by Chris Done
(Chinese Translation by Mort Yao)


翻译君的话

为了给了解一些Lisp的人节省时间:你可以把Z看成是Scheme在另一个平行宇宙里的镜像——Scheme程序由S-表达式(S-expression)构成,而Z的基本元素则是Z-表达式(Z-expression)。Z-表达式与S-表达式的不同之处在于,它通过缩进而不是括号嵌套来表示其语义。如果你知道了这些,就可以直接去hack它了:

https://github.com/chrisdone/z

(注意:该项目虽然名字叫Z,但是和由Zermelo集合论衍生而来的Z符号语言似乎并无直接联系。)

以前有个笑话:据说某俄国特工经过九死一生偷到了NASA的太空火箭发射程序的源代码的最后一页,代码是

)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

现在,如果你厌倦了Lisp里面太多的括号,不妨去尝试一下Z!

这个由Chris Done发起的项目比较新(2013年1月1日,也就是前几天刚发布)。查了一下Scheme的历史,发现采用缩进来代替括号的想法早在十年前(2003年)就有人在SRFI(Scheme Request For Implementation)上面提出过了(SRFI-49: Indentation-sensitive syntax)。当时用的名称叫做I-表达式(I-expression)。不过这个想法并没有被Scheme社区广泛接受。不知道这次新出现的Z前景如何,拭目以待。

此外,如果你做过传说中的48小时写一个Scheme解释器(Write Yourself a Scheme in 48 Hours)的话,你会发现这个Z解释器的实现基本上是与它平行的。把以前实现的Scheme解释器改写成Z解释器应该也不是什么难事。玩的只是概念而已。

以下为原文内容。


一种叫做Z的小型语言

一种小型的、严格的、非纯函数式的、柯里式的(curried)、动态类型的(尽管这可能会在将来改变)、分步代入的(partially applied)且有着相当奇特语法的语言。

它的基本思想看起来很聪明,甚至聪明得有些过头了。 — 摘自reddit的评论

从Markdown得到的启发

首先,让我们回顾一下Markdown。即使你没有亲自写过Markdown,你应该也已经见到过一些。你应该知道在Markdown的语法中有一个特别之处,即它嵌入代码的方式。它极其简单;不过是:你只需缩进4个空格,然后就可以随心所欲地在后面书写任何代码了!

Hello world! Here comes some code!

    Here is some arbitrary code! f.x()/f23(); // Zaha!

And now we're back to normal text...

这个想法的特别之处,就是你事实上可以把任何东西都放到这个缩进的后面,而且,它们不会影响到外围的代码!这是个非常牛逼的想法,让我来告诉你为什么。

Z-表达式(Z-expression)

我将在此介绍一个极其微型的语言,称之为“Z”,用来展示我的这一想法。

Z有着极其、极其简单的语法。古怪,却简单。这里是它如何工作的一个例子,函数的调用采取如下形式:

name argument

更深入些的话,下面这段代码:

foo bar mu zot

实际上是被分组后依次解析的:

foo (bar (mu zot))

(注意:在Z源码里实际上并没有括号。Z即Zero。)

如果仔细想一想前面给出的name argument形式的话,这其实是一个非常自然的分组方式。

为了向某个函数传递多个参数,额外的参数需要被放置在下一行,并且被统一缩进到与第一个参数所在的同一列。

foo bar
    mu
    zot

这表明foo函数有三个参数。该规则适用于任何场合,因此,我们也可以写:

foo bar mu
        zot
    bob

这表明foo函数有两个参数,而bar函数有两个参数。

我管这种形式叫做“Z-表达式(Z-expression)”。Lisp语言是括弧状的(curly),卷曲的(curvy),它有它的S-表达式(S-expressions)。而Z则是锯齿状的(jagged),犀利的(sharp)。以及,古怪的。

特殊算符遵循一套相同的规则。下面我将介绍其中的一些特殊算符。

Z的内置算符

defun特殊算符需要两个参数:一个名称的列表,其中第一个表示函数的名称;以及一个函数体。这里是一个用来连接两个列表的函数:

defun ap x y
      ++ x
         y

所有的Z函数都是柯里式的(curried)和分步代入(partially applied)的,如同在Haskell中一样。故上述代码等效于:

def ap
    fn x
       fn y
          ++ x
             y

在本介绍中暂时不对此加以赘述。我们同样有ifdo

if foo
   bar
   mu

do this
   that
   those

如果你感兴趣的话,注意下,这些特殊算符采取一种非函数的常序式求值策略(Normal-order evaluation)。它们只是单纯从语法上来解释自身的参数!

我们同样有一些用于表示数字的诸如123、用于表示字符串的诸如"strings"unit之类的符号,就和nothing、null、empty、voidness、niente一样稀松平常(翻译君:这些是各种用来表示“空”的词汇)

定义宏

啊哈!La pièce de résistance(法语:主盘大菜上桌,即全套大餐的精华)!现在,我们将拥有一个defmacro算符,它的任务是允许我们自行定义新的符号。看好了……

defmacro -- _
         "unit"

Voilà(法语:瞧这)!我们定义了一个名称--,它取得一个我们并不关心其内容的参数_(翻译君:在这里下划线的含义与许多语言中相同,表示无名变量),并且总是返回字符串"unit"

所有的宏都读取一个字符串,这个字符串是源码中所有可以成为它参数的部分,而我们知道,这部分将由缩进来决定。所有的宏都将会产生一个新的字符串,被用来置换到调用宏的原位置,然后将会被解释器重新解析

在我们的这个--宏的例子当中,我们仅仅返回了一个unit,一个非操作符(no-op)。这样,我们就已经定义了属于我们自己的注释符号

-- A simple function, that is used inside the macro below.
defmacro ap x y
         ++ x
            y

挞哒(Tada)!这是一个带注释的函数定义!用到了我们刚才自创的注释语法,不是吗!我们同样也可以把这个函数ap用在其它的宏的内部,这在Lisp语言中是非常典型的用法。所以现在,就让我们基于它来定义一个稍微复杂一些的宏吧:

when

-- A messy macro (because it uses string manipulation),
   but demonstrates the idea well enough.
defmacro when input
         fn blocks
            ap "if"
               ++ z:indent-before 3
                                  car blocks
                  ++ "\n"
                     ++ z:indent 3
                                 car cdr blocks
                        ++ "\n"
                           z:indent 3
                                    "unit"
            z:blocks input

这里可以看到,我们提供了一些辅助函数,用以获取“blocks”——也就是一次函数代入中的全部参数——并将它们传递给从fn blocks开始的匿名函数,然后构造出一个用于返回的字符串。

你能看出这个宏的目的吗?有了它,我们就可以写出:

when = 1
       1
     print ++ "The number is: "
              when true
                   show 123

看看这是多么自然?在宏当中嵌套宏什么的完全不是问题!

字符串的宏

在编程时经常会遇到的问题是,如何尽可能避免麻烦地书写字符串。通常情况下,我们得遵循一套字符转义的特殊规则。但在Z中,你完全不需要这么做!

以前字符串的使用方式都弱爆了:

print "Hai, guys!"

我们将要定义一个新的宏来方便我们书写字符串,那就是:符号。它旨在让字符串同正常人类语言一样直接可读,并且允许你在缩进后的文本列范围内不受任何限制地书写任意文本。

defmacro : input
         z:string input

以上,我提供了一种将一个string转换成一个"string"的方法,因此不管把任何东西作为input传递到宏,都将被逐字返回,并且是以字符串的形式。准备好了吗?请看!

-- Example with print:
print : Hello, World!
        What's going on in here? 

果然碉堡了不是么?它看起来就像是一段常见的脚本程序!而,也正是从Markdown那里得到的启发。它同样也适用于在其他函数中的代入:

defun message msg
      do print : Here's a message
         print msg
         print : End of message. 

可以如此使用上述定义的函数:

message ap : Hello,
           ++ " World! "
              : Love ya! 

只要你愿意,当然也可以直接这么写:

message : Everybody dance now!

一些函数定义的示例

到目前为止已经足够给力了。让我们从兴奋中暂时缓口气,来看一些枯燥的纯函数。这些就是Z代码真实的样子。

-- Map function.
defun map f xs
      if unit? xs
         unit
         cons f car xs
              map f
                  cdr xs

-- ["foo","bar"] → foo\nbar\n
defun unlines xs
      if unit? xs
         ""
         ++ car xs
            ++ "\n"
               unlines cdr xs

-- Take the first n elements of list xs.
defun take n xs
      if = n
           0
         unit
         if unit? xs
            unit
            cons car xs
                 take - n
                        1
                      cdr xs

-- Take all but the last element of a list.
defun init xs
      if unit? xs
         unit
         if unit? cdr xs
            unit
            cons car xs
                 init cdr xs

-- Take the last element of a list, or return a default.
defun last def xs
      if unit? xs
         def
         if unit? cdr xs
            car xs
            last def
                 cdr xs

话说,没有模式匹配(pattern matching)的编程不是很蛋疼吗!?不幸的是,今天我们暂时不会在这里用Z去定义一套模式匹配符,因为要实现一个像模像样的模式匹配并非轻易之举,而做一个过于简陋的会让我感觉很囧。(翻译君:囧rz)

我们可以开始使用这些函数了,正如所期望的那样:

-- Print the blocks of foo and bar with ! on the end.
print unlines map fn x
                     ++ x
                        "!"
                  z:blocks : foo
                             bar

-- Use of take function.
print unlines take 3
                   z:blocks : foo
                              bar
                              mu
                              zot

正则表达式

这是另外一个简单的宏的实际用途:正则表达式!让我们来亲身体验一下。

我们的标准库中最基本的正则函数是regex:matchregex:newregex:match返回一个所有匹配正则表达式(foo)的列表(list)。

print regex:match regex:new "(abc)"
                  "abc"

到目前为止我们已经算得上是宏的高玩了,所以,来看看更漂亮的写法是什么样的:

defun ~~ regex string
      regex:match regex
                  string

print ~~ regex:new "(def)"
         "defghi"

元芳,你怎么看?还不错吧?至少要做一个字符串匹配所需的代码变得更短了。不过构造正则表达式的语法仍然显得有些笨重。让我们来创建一个宏!

defmacro rx input
         ++ "regex:new "
            z:string input

print ~~ rx Age: (.*)
         "Age: 123"

稍微美观了些,但是还算不上碉堡

我们或许可以跳过整个构造正则表达式的部分,把它与匹配的部分合并:

defmacro ~ input
         fn blocks
            ++ "~~ rx"
               ++ z:indent-before 6
                                  unlines init blocks
                  ++ "\n"
                     z:indent 3
                              last ""
                                   blocks
            z:blocks input

print ~ Age: (.*)
        "Age: 666"

现在我们才算是真正完成了屌丝的逆袭!这语法看起来就像是从矮穷挫瞬间变成了高富帅有木有!

print ~ Age: (.*)
        ([a-z]+)
        "Age: 777\nlalala"

更加高能的是,你甚至可以把正则表达式分开在多行写。卧槽,既然这么给力,再来和正则战个痛又如何?

print ~ Age: (.*)
        ([a-z]+)
        : Age: 999
          beep!

当然,它也可以与其它的宏在一起搭配使用。管他后面这句话是什么意思反正翻译君是不想翻了。

代码编辑

Z-表达式的另外一个撸点是,对它进行文本编辑几乎毫无鸭梨。试问:你如何才能在Lisp或者其他类似语言中找到当前语义节点的起始和终止位置?

(lorem ipsum-lorem-ipsum ()
  (foo-bar)
  (let* ((zot (biff-pop))
         (zar-zar (beep "%s.bill" bob)))
    (if (ben-bill-bocky doo-dar)
        (let*| ((foo (foo-bar-mu-zot))
               (bar (ipsum-lorem))
               (ipsum (cdr (assoc 'cakes lorem)))
               (lorem (cdr (assoc 'potato lorem)))
               (ipsum (cdr (assoc 'ipsum lorem)))
               (lorem (cdr (assoc 'lorem lorem))))
          (if bob
              (progn
                (bill ben)
                (the cake is a lie)
                (the game))
            (message "Structural integrity is not secured.")))
      (message "Data, because it's polite." cakes))))

如果你的光标刚好处在let之后,你会怎样做?老套路。你开始搜寻一个标志着起始的左括号。你找到了它。然后你开始向后移,搜寻一个标志结束的右括号。每次当遇到一个左括号,你把它压入栈中。每次当遇到一个右括号,你把它从栈中弹出。一旦遇到了一个字符串的开始,或者是转义字符序列,你将需要等待再次遇到下一个非转义字符,然后继续……呃,你开始感到乏味了吗?其实我也这么想。我以为我能轻易地办到这件事情,但事实上并非如此。(翻译君:Emacs下难道没有方便定位Lisp代码的插件吗?我什么都不知道……)

在Z中,一切都变得如此简单。你只需找到起始的列,根据第一个非空格字符的位置。然后上下移动光标,找到与其缩进的起始列位置不同的行,那就是当前语义块起始或终止之处。你将拥有整个Z-表达式。想要移动它?小菜一碟,只需要剪切和粘贴、之后根据偏移增添或删减前置空格即可。担心缩进风格?在Z中它算不上什么问题。就像不存在缩进风格这回事一样。Z自始至终只有一种缩进方式。

未来的工作

反引号(Quasiquotations)

若不借鉴前人的历史,我们必将一事无成。Lisp有着悠久的历史,它教给了我们引号和反引号,以及借助于它们来处理字符串是多么便捷。我同意这一点。这就是为什么在下一步,我打算实现这样的语法:

defmacro when cond body
         ` if , cond
              , body
              unit

当然,它遵从与所有Z-表达式相同的语法模式,但却与Lisp具有相同的语义。无论如何,这仅仅只是一个语法糖而已。Z的真正威力体现在它通过缩进来划分代码的语义区域上。

“数学”宏

在Z中,你使用多参数函数时需要用到缩进。这在做某些数学计算的时候多少有些蛋疼,因为数学表达式的参数通常只是一些简单的、同序的子表达式。考虑到这一点,拥有一套数学宏是完全必要的。例如,#

def x # x²-y²×(2xy+x²-y²×(2xy+c))

为什么不呢?(翻译君数死早,没看出来这个式子是要干啥= =)


安装入门

实现

这里是Z的官方解释器。

安装

Z解释器当前使用Haskell实现。你可以在这里下载Haskell。

Haskell安装完成后,编译Z的解释器:(翻译君:你可能需要自行用包管理器安装textparsec

$ cd <path/to/z/dir>
$ ghc Setup.hs
$ ./Setup configure
$ ./Setup build

生成的二进制文件位于dist/build/z/z

运行它,可以交互式地对Z语句进行求值。

若要执行一个完整的Z源文件,可以通过管道:

$ ./dist/build/z/z < examples.zz