lcomplete's Blog

一个带着极客精神正努力成为黑客的程序员

七周七语言之用Io编写领域特定语言

| Comments

Io 语言是在2002年创造出来的,虽然距今已经有11个年头了,但是对于一门编程语言来说,它还只能算是一门年轻的语言。Io 并不是主流编程语言,没有什么名气,就连名字取的也并不适合推广,io 有太多其他的含义了,用google直接搜索 io 的话,很难找到关于这门编程语言的资料,英文推荐搜索“io language”,中文则建议搜索“io 编程语言”。

Io 的优缺点

Io 是个小众语言,文档不足,这是其中的一个缺点,但是这门语言吸收了许多语言的特点,具有很高的学习价值。Io 的设计借鉴了 SmallTalk、Self、NewtonScript、Act1、LISP和 Lua 的一些特点,它是一门基于原型式设计的编程语言,具有面向对象特性,语法概念极少,它没有关键字,所以能够在很短的时间内熟悉语法,Io 还有出色的并发库,利用它能够以简单直观的方式编写并发程序。

Io 有很少的语法规则和语言特性,这似乎在某种程度上束缚了它的能力,但是使用 Io 可以很容易地扩展这门语言自身的语法,我们可以自定义运算符或改变运算符的行为,也可以使用元编程在任何时候改变任何对象的行为。正是这些高扩展性和可定制性使得 Io 在创造领域特定语言(Domain Specific Language,简称DSL)方面具有非常强大的能力,我们可以利用它创造出自己喜欢的语法,编写出能够编写程序的程序,学习 Io 颇具启发性,怎样利用 Io 打造出能够提高编程效率的 DSL 更是一个值得深入研究的课题。

什么是领域特定语言?

Martin Flower 在《领域特定语言》一书中给出的定义是:针对某一特定语言,具有受限表达性的一种计算机程序设计语言。DSL 不是图灵完备的,所以它的表达能力有限;由于它是领域特定的,它的出现往往是为了消除业务领域的复杂性,所以 DSL 一般是整洁的、易于使用且接近自然语言的。我们平时在编码的过程中其实会接触到很多领域特定语言,比如 CSS、HTML、XML、JSON、正则表达式、XAML,他们都是针对某一领域的,“求专不求全”,CSS用于定义Web页面样式;HTML用于定义Web界面元素;XML、JSON用于描述数据,这些数据可以作为纯数据,也可作为某种行为的配置。

DSL 从实现角度分为内部DSL和外部DSL,用XML来描述某种行为就是一种常见的外部DSL的形式,利用编程语言自带的语法结构定义出来的 DSL 称为内部DSL,比如时下流行的流畅接口(Fluent Interface)编码方式就是一种内部DSL。上面说到 Io 编程语言可以自定义运算符且具有元编程特性,因此 Io 可以方便的创造出内部DSL,下面我们就通过几个示例来看一看 Io 在这方面的能力。

使用 Io 创造内部DSL

先看一个比较简单的例子,可以说在所有的编程语言中都有列表和字典(一些语言中也称为映射)这两种数据结构,在 io 中也不例外,io 中创建列表对象可以使用 list 函数,比如 foo := list(1,2) ,这很方便,然而创建一个字典的写法却比较繁琐,你需要 clone 一个 Map 对象,然后依次调用 atPut 方法向其中添加键值对,作为一个常用的数据结构,这样写不仅会影响效率而且会使代码变的臃肿,纵观其他编程语言,大部分都有相应的字典初始化语法糖,常用的写法为 {“key”:”value”} 结构,下面我们就在 io 中来创造这种语法糖。

dict.io
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
OperatorTable addAssignOperator(":", "atPutNumber")

Map atPutNumber := method(
  self atPut(
    call evalArgAt(0) asMutable removePrefix("\"") removeSuffix("\""),
    call evalArgAt(1)
  )
)

curlyBrackets := method(
  data := Map clone
  call message arguments foreach(arg,
    data doMessage(arg))
  data
)

squareBrackets := method(
    arr := list()
    call message arguments foreach(arg,
        arr push(call sender doMessage(arg))
    )
    arr
)

doFile("dict_sample.io")

先简单说明一下上面这段代码,OperatorTable 中保存了可用的操作符号表,我们可以使用 addAssignOperator 方法向其中添加用于赋值的操作符号,并将其映射到第二个参数所指定的方法中,第一行代码的意思为将 : 符号映射到 atPutNumber 方法中,当在代码中碰到 : 符号时,将调用 atPutNumber 方法;curlyBracketssquareBrackets 为 io 中的特殊变量,将方法赋值给它们相当于改变 {} 和 [] 符号的行为,在我们的代码中,指定了当碰到 { 符号时初始化 Map 对象,接着循环处理参数,这里的参数是用 , 符号分段的,data doMessage 方法相当于以data为上下文执行参数中的代码,参数代码中若碰到 : 符号则会调用 atPutNumber 方法,将符号左边的参数当作键,右边的参数当作值存入data中,最后返回data。

注意,最后一行的 doFile 方法是执行另一文件中的代码的意思,为什么这个例子要分成两个文件呢?主要是因为虽然我们在操作符号表里面添加了一个符号,但是在这个文件的执行上下文中,是不会起效果的,只有调用 doString 或 doFile 才会起效,所以将创建字典的代码放在了下面这个文件中。

dict_sample.io
1
2
3
4
5
6
7
dict := {
  "name": "lcomplete",
  "array": [1, "str"],
  "nested": {
    "hello" : "io"
  }
}

很简洁吧,而且还支持嵌套定义哦。

通过第一个例子,相信各位已经能看出 io 在创造内部dsl上的灵活性了,接下来我们看一个更强大的例子,使用 io 让编写html变的更有禅意,何谓禅意,其实是受到 Sublime 上的 ZenCode 插件的启发,使用 ZenCode 语法可以快速的编写 html 片段。这次我们先看一下使用 dsl 的方式。

enhance_html.io
1
zencode(Html html>head+body>div!content$id@class*3)

不难发现,这个语法已经跟宿主语言几乎没有任何关系,是一门彻底的内部dsl,上面这段代码生成的 html 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
  <head>
  </head>
  <body>
    <div id="id" class="class">
      content
    </div>
    <div id="id" class="class">
      content
    </div>
    <div id="id" class="class">
      content
    </div>
  </body>
</html>

很强大吧,一行代码就可以生成出这么多的 html,现在我们再看看这个 dsl 是如何定义出来的。

zencode.io
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
forward := method(
    call message name
)

root := nil

Html := Object clone do(
    OperatorTable addOperator("@", 15)
    OperatorTable addOperator("$", 15)
    OperatorTable addOperator(">", 15)
    OperatorTable addOperator("+", 15)
    OperatorTable addOperator("*", 15)
    OperatorTable addOperator("!", 15)

    @ := method(classname,
        attrs append(" class=\"#{classname}\"" interpolate)
        self
    )

    $ := method(id,
        attrs append(" id=\"#{id}\"" interpolate)
        self
    )

    > := method(tag,
        html := Html clone
        html tag := tag
        html parent := self
        self childs append(html)
        html
    )

    + := method(tag,
        html := nil
        if(self parent,
            html = Html clone
            html tag := tag
            self parent childs append(html)
        )
        html
    )

    * := method(count,
        self count := count asNumber
        self
    )

    ! := method(content,
        self content := content
        self
    )

    forward := method(
        name := call message name
        html := Html clone
        html tag := name
        root = html
        html
    )

    init := method(
        self childs := list()
        self attrs := list()
        self count := 1
        self parent := nil
        self content := nil
    )
)

Html flush := method(
    root render("")
)

Html render := method(indent,
    self count repeat(
        writeln(indent,"<",self tag,self attrs join(""),">")
        childIndent := indent .. "  "
        if(self content,
            writeln(childIndent,self content)
        )
        self childs foreach(index,arg,
            arg render(childIndent)
        )
        writeln(indent,"</",self tag,">")
    )
)


zencode := method(html,
    html flush
)

doFile("enhance_html.io")

有几处需要说明一下,forward 是一个特殊方法,相当于 ruby 中的 method_missing ,当调用的方法不存在时,系统将会把调用转向到 forward 方法,这就给了我们很大的灵活性,我们无需定义 html、head 等方法,这些未定义的方法会自动转向到 forward 中处理;注意,一共有两个 forward 方法,分别应用在 Object 和 Html 对象上,只有 Html html 会返回 Html 对象,其他的未定义方法会转向到 Object 的 forward 方法上,这里直接将方法名作为字符串返回,新添加的 @、$ 等操作符,将会把接受到的字符串进行处理,最后再返回一个 Html 对象以进行链式调用。

总结

在 Io 中可以轻松地定义操作符、实现动态方法或更改所有对象的行为,这些能力是强大的,也是危险的,使用之前需要仔细衡量。当然,如果只是用它来做某一件事情,比如用来编写上面的 ZenCode 时,那么可以大胆发挥自己的想象力,在它的基础上构建一门属于自己的语言。

Comments