cora 语言介绍

2019-07-22

cora 语言是我设计的一门 lisp 方言。又是一门 lisp?是呀!每个真正的 lisp 程序员都应该发明自己的语言的。。lisp 我一直在学习,关于这门语言我思考了许多年,最终把我喜欢哪些 feature 写下来,也算作是做一个交待吧。

实现一门语言的解释器或者编译器,跟设计一门语言,其实不是同一种乐趣。实现是由别人定好了标准,自己只是按标准做出来。而设计语言的时候,需要了解许许多多语言,知道它们设计背后的思考。然后再决定,哪些 feature 是自己语言里面需要的,哪些是不需要的,这是需要更多审美。另外,用自己设计的语言写东西,是一件非常有成就感的事情

在这里简单介绍一下这门语言。

cora 受到的最主要的影响来自于 scheme 和 shen。当然,考虑到我接触过的那么多语言,也不能排除一些影响来自于其它的项目,比如 femtolisp,比如 ocaml,甚至是 Go。

目前只是做了一个解释器实现,而且代码还非常糙(0.1 版本都没到,迫不急待就来写博客,向世界宣布:hello, world)。代码在这里:https://github.com/tiancaiamao/cora

使用很简单,下载代码,然后 go build 生成 binary。然后 ./cora 就会进入到 repl 了。

闭包是可视化的

这个是 "借鉴" femtolisp 的做法:直接在 lambda 表后面追加环境,环境用的是关联表表示的。

(lambda (x) x) ;; 一个闭包
(lambda (a) 3 (b . 5) (c . 7)) ;; 在 ((b . 5) (c . 7)) 环境下面的闭包

比如执行一下:

➜  cora git:(master) ./cora
1 #> ((lambda (a) (lambda (b) a)) 42)
(lambda (b) a (a . 42))

这么设计,跟 scheme 语法会有冲突,lambda 的 body 只能是单个 expression。如果要多个,就要按这种写法:

(lambda () (begin 1 2 3))

极简内核

特殊表只有 quote lambda if do macro 等非常少的几个。理论上,macro 也不是一定要做成特殊表的,只不过目前先这么实现着。

所有表达式经过完全的展开后,都会变成最后很少的几个特殊表。因为特殊表是会实现在最核心层的东西,越少越好。

像 defun let defmacro cond list 等等等这些东西都可以做成宏。

0 #> list
(macro exp (rcons (cdr exp)))
1 #> let
(macro exp (rewrite-let () () (cdr exp)))
2 #> or
(macro exp (if (cadr exp) true (caddr exp)))

甚至连 set 都是函数而不是特殊表。

(set 'or (macro exp (list 'if (cadr exp) true (caddr exp))))

我不喜欢括号过多,let 宏是用了更像 shen 那边的表示:

(let a 3 b 5 ...)

这明显比在 scheme 里面的写法要简单:

(let ((a 3) (b 5)) ...)

first class macro

first class macro 并不是一个特意设计的语言特性,只是由于目前是解释器,并且有 macro 特殊表。所以在当前实现中宏是 first class 的。 (macro xxx) 可以传递给变量,可以当作返回值...

8 #> (set 'mylet let)
(macro exp (rewrite-let () () (cdr exp)))
9 #> (mylet a 3 b 5 (+ a b))
8
(defmacro m (exp) body)

实现上仅仅是给变量 m 绑定一个 macro 特殊表。

而到调用 (m xxx) 的时候会先 eval a,发现结果是一个宏,然后使用宏展开,再进行展开之后的 eval。

真正可以算语言设计层面的,是故意采纳了非卫生宏。卫生宏的实现更复杂,而且我感觉属于偏学术而不是真正解决实际问题的特性,因此在 cora 里面没有采用。

严格尾递归

cora 是要求严格尾递归的,这点跟 scheme 强调的一样。这个 feature 成为必须的理由,是来自于我在实现宏的过程中的观察。如果不支持尾递归,在做 pattern match 宏的时候很容易展开后的代码变成非尾递归的,然后爆栈。

比如这么写代码,总不能爆栈吧:

(func f
      0 => 42
      n => (f (- n 1)))
(f 9999999)

partial apply

shen 语言是自动 partial apply 的。我在使用过之后觉得,真香!

自动 partial apply 可以避免很多无用的代码,相比于

(map (lambda (x) (+ x 1)) l)

这样子肯定是更简洁的:

(map (+ 1) l)

在 scheme 里面不必要的重复定义,如果支持 partial apply 就根本不用那么复杂。 本来用宏也可以做一些简化,像 clojure 里面有一些缩写,但是我不太喜欢 sugar 过多。

语言内置 pattern match

pattern match 是一个非常有用的 feature。本来可以做成库的,但是这个重要程度让我觉得,应该由语言内置。

(match (cons 1 2)
  42 42
  's 666
  (cons a b) a
  (list x y z) x
  (list-rest a b) b
  x x)

其实我之前已经提过了,应该 让 pattern 匹配的对象,跟构造这个对象使用的同一种语法

值得一提的是 list-rest。list-rest 是将前面的 pattern 匹配到单个变量,而最后一个变量匹配剩下的链表部分。比如:

3 #> (match (list 1 2 3 4 5 6) (list-rest a b c d) d)
(4 5 6)

这里面 a b c 分别会匹配 1 2 3,而剩下的部分 d 匹配到 (4 5 6)

其它的理解起来都很简单,42 匹配常量;(quote s) 匹配符号 s(cons a b) 匹配一个 cons,(list x y z) 匹配 3 个元素的链表;上面的表达式最后会返回 1。

所有模式都是可组合的,比如这种 (cons (list a 1) 'xx)

func 宏

cora 鼓励优先使用 func 来定义函数。func 是一个宏,这个 syntax 明显是受了 Go 的启发,使用是这样子:

(func filter-h
      res fn () => (reverse res)
      res fn (cons hd tl) => (filter-h (cons hd res) fn tl) where (fn hd)
      res fn (cons _ tl) => (filter-h res fn tl))

=> 前面是模式,匹配函数的输入,后面是执行的操作。可以带 where 过滤条件。

用 pattern match 写出来比 defun 会好读一些。

(defun filter-h (res fn l)
  (if (cons? l)
      (if (fn (car l))
          (filter-h (cons (car l) res) fn (cdr l))
          (filter-h res fn (cdr l)))
      (reverse res)))

好啦,下面才是正经的介绍了:lisp 基础,可以在 cora 里面练习一下。

TL;DR

数字,字符串,布尔的 true 和 false 是常量,常量求值得到自身:

4 #> 1
1
5 #> "asdf"
"asdf"
6 #> true
true
7 #> false
false

quote 特殊表,缩写是单引号 ',被 quote 的东西不求值,可以用它得到符号:

8 #> 'asdf
asdf
9 #> (quote asd)
asd
10 #> (quote (a b c))
(a b c)

变量,在 lambda 或者 let 这些东西里面,参数都是变量:

11 #> (lambda (a) a)
(lambda (a) a)   ;; 这里面的 a
12 #> (let a 3 b 5 (+ a b))  ;; 这个的 a 和 b
8

if 表达式:

13 #> (if true 1 2) 
1

lambda 表达式,也就是定义函数:

17 #> (lambda (x) x)
(lambda (x) x)
18 #> ((lambda (x) x) 42)
42

set 是一个函数,它接受的参数是一个符号和一个值,即设置符号绑定的值:

14 #> (set 'a 3)
3
15 #> (set 'b 5)
5
16 #> (+ a b)
8

注意这个绑定是全局绑定的,跟变量不太一样。定义函数实际上就是为符号,绑定一个 lambda 表达式,比如:

(defun id (x) x)

等价于

(set 'id (lambda (x) x))

可以验证一下:

19 #> (defun id (x) x)
(lambda (x) x)
20 #> id
(lambda (x) x)
21 #> (set 'id (lambda (x) x))
(lambda (x) x)

函数调用

22 #> (id 42)
42

相互递归定义也是可以的

23 #> (defun even (x)
  (if (= x 0)
      true
      (odd (- x 1))))
(lambda (x) (if (= x 0) true (odd (- x 1))))
24 #> (defun odd (x)
  (if (= x 1)
      true
      (even (- x 1))))
(lambda (x) (if (= x 1) true (even (- x 1))))
25 #> (even 30)
true
26 #> (odd 31)
true

完。

lispcora