【译文】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
在本介绍中暂时不对此加以赘述。我们同样有if
和do
:
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:match
和regex:new
。regex: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的解释器:(翻译君:你可能需要自行用包管理器安装text和parsec)
$ cd <path/to/z/dir>
$ ghc Setup.hs
$ ./Setup configure
$ ./Setup build
生成的二进制文件位于dist/build/z/z
。
运行它,可以交互式地对Z语句进行求值。
若要执行一个完整的Z源文件,可以通过管道:
$ ./dist/build/z/z < examples.zz