What is Lisa?
Lisa 是用Scala写的一门Lisp方言及其解释器,运行在JVM上。 因为是Lisp的方言,Lisa是动态类型的。 Lisa的语法受到了Lisp和Elixir的启发,和两者有许多相似之处(抄了很多)。 我们知道,一门解释性的语言的性能是很弱的(尤其是动态类型的语言),因此Lisa不是为了性能而生的,她在一开始是作为一门JVM上的胶水语言而设计的,为了能方便调用Scala/Java编写的API。我们知道,一个正常人很难喜欢上用Lisp系语言写工业代码,因此,Lisa不是为了成为一门工业级代码而设计的,甚至不是为了让人类编写代码而设计的。那你猜猜是谁要去编写Lisa代码呢?
本文是Lisa的一个简要文档,如果你有Lisp或Erlang/Elixir的背景的话,你应该能很快理解。
基本类型
- 1 => Int
- 2.3 => Float
- "s" => String
- false => Bool
- :atom => Atom
复合类型
Seq, List
序列和列表可以通过内建的函数seq
和list
构造。
list
: 构造一个List
seq
: 构造一个scala.collection.immutable.Vector
(seq 1 2 3)
=> #Scala(Vector(1, 2, 3))
(.[0] s)
=> 访问序列的 [0]
元素,同理,.[1]
访问[1]
元素……
你也可以使用nth
来访问序列里的元素。如果下表越界,会抛出一个IndexOutOfBoundsException
。(nth s 1)
访问序列1
元素(第二个元素)。同时,你也可以传入第三个参数,为不存在该元素时的默认值。另外,get
函数会在当元素不存在的时候返回()
。
在大部分lisp方言中,()
和 '()
是相等的,lisa也是如此。在大多数情况,()
会被认为是一个空列表,但是()
和'()
不是同一个引用。因此,你可以使用same-reference?
来区分他们。
列表和代码
在Lisa中,列表(List)和代码其实没有本质区别,列表可以是可执行的代码,代码也可以是一个列表。列表会被求值(evaluate),直到成为环境中不可再规约的值。
可以通过引用(quote)来避免对一个列表进行求值。quote在语法上就是相应的表达式前加上'
。
存在两种quoting:
- Quoting 避免代码的求值,在表达式前加
'
- Quassi Quoting 避免部分代码的求值,允许部分代码被求值,语法为在表达式前加`'。
为了在QuassiQutation中插入被求值的代码,需要使用UnQuote操作,语法是在表达式前加~
。
为了在一个list中拍平另一个list,需要使用UnquoteSplicing操作,语法是在表达式前加~...
。
例如:
(define a 1) ; Defined a variable a to 1
'(define a 1) ; A list contains 'define 'a and 1
'(1 2 3 4) ; A list contains 1 2 3 and 4.
(1 2 3 4) ; Oops, can not apply 1 to '(2 3 4)
`'(~a 2 3 4) ; '(1 2 3 4) because a is bound to 1
`'(1 ~...(range 2 4) 4) ; '(1 2 3 4) flatten (2 3) to the list.
Quote 在宏中发挥了至关重要的作用,宏的目标就是运行时生成代码,并插入被调用的地方求值。
记录类型(Record)
记录类型和Map
十分相似。
可以通过 record
函数构造.
(record 'a 1 'b 2)
=> 构建一个记录,类似于{a: 1, b: 2}
record
的键可以是一个字符串,也可以是一个symbol
,同样值的string
和symbol
代表着同样的键。
记录 s
的a
属性可以通过以下方式访问:
- (.a s) 以Java/Scala 访问对象属性的方式
- (s 'a) 以Lisa调用对象方法的方式。
- (get s "a") 通过get函数
get函数可以用以下方式调用:
(get map key)
(get map key not-found)
map
是一个record
key
是一个键not-found
是record没有这个键时返回的值,默认为()
(record-contains? record key)
检查一个记录里面有无指定的键,返回一个Bool。
(record-updated record key value)
更新一个记录。
数值计算
和Elixir/Python一样,整数和小数都是高精度的。同时,Lisa支持有理数运算。
(/ 2 3) ; => 2/3
(* (/ 1 2) (/ 3 4) (/ 5 6)); => 5/16
(* (/ 5 16) 0.1) ; => 0.03125
(+ (/ 1 2) (/ 3 4)); => 5/4
(int (/ 5 4)); => 1
(< 2 3 4) ;=> true
(< 2 4 3) ;=> false, because 4 is greater than 3
(<= 1 1 1 1) ; => true
语法
和Scheme
语法基本一致。
(define n 0) ; 声明一个变量绑定
(define (cons x y) ; 定义一个函数
(lambda (m) (m x y)))
(define car ; 等同于声明一个lambda表达式值。
(lambda (m)
(m (lambda (x y) x))))
(define (fact n)
(define (go n x) ; 在函数内定义函数不会影响外部作用域。(函数会引入一个新的作用域)
(println! "Computing:" n) ; 使用 println! 来显示若干字符串,用空格分割。
(if (= n 0) x (go (- n 1) (* n x)))); 函数中最后一个表达式是返回值。
(go n 1)); 调用一个内部定义的函数。
(define (fib n)
(cond ((< n 2) n)
(else (+ (fib (- n 1)) (fib (- n 2))))))
(let ((a 1) (b 2)) (+ a b)) ; <=>
((lambda (a b) (+ a b)) 1 2)
特殊变量
由于和Java的互操作性,点(.
)开头的变量有特殊的含义,表示访问这个对象的某属性或者调用方法。
(.length "hello!"); <=> "hello!".length => 6
(.startsWith "Lisa" "L" ); =>"Lisa".startsWith("L")
(.[1] (seq 1 2 3)) ; => (new int[] {1, 2, 3})[1] => 2
函数参数模式匹配
和Elixir相同的是,函数形参赋予实参的时候,是通过模式匹配实现的。
倘如我们写一个阶乘函数:
(define (fact 0) 1)
(define (fact n) (* n (fact (- n 1))))
这种函数在Lisa中被称为多态函数
,每一个情况被称为分支(branch)。
在模式匹配中,相同的变量代表相同的值(对于闭包或者函数,则是代表相同的对象)。 比如写相等函数可以这么写:
(define (equals? x x) true)
(define (equals? x y) false)
特别地,当你想要匹配在上下文中相同的变量时,可以使用反引号
(define x 0)
(define (zero? `x`) true) ; 仅仅匹配`x` (0)
(define (zero? x) false) ; 匹配任意值
Pattern Matching Guard
模式匹配最后一个参数可以是守卫,代表匹配到的参数必须满足的要求。形如(when <predicate>)
,这里的when
可以是:
when
代表这个表达式必须成功被执行,否则返回失败。when?
表示如果该表达式无法执行,就忽略这个分支,往下面定义的同名函数中寻找匹配。
when?
在标准库中常被用来判断类型(如果一个对象满足某种约束,他就属于那个类型)。
(define (fact 0) 1) ; Nothing interesting.
(define (fact n (when (> n 0))) (* n (fact (- n 1)))) ; 当n > 0的时候才会匹配
(fact 5) ; => 120
(fact -5) ; => No matching procedure to apply
尾调用优化
Lisa能进行尾调用优化。当一个函数的最后一个表达式是调用另外一个函数的时候,栈深度不会增加。
注意:和大部分具有尾调用优化的语言不同的是,如果最后一个表达式是if
的话,不会对该函数进行尾调用优化。因为if
语句在Lisa中不是一个控制语句而是一个表达式。
例如:
(define (overflow-fact n)
(define (f n x)
(if (= n 0) x (f (- n 1) (* n x)))) ; 这里不会进行尾递归优化
(f n 1))
(overflow-fact 1000) ; => Boom!
幸运的是,在大多数情况下,因为Lisa存在模式匹配,你不大需要用到if
和cond
。
(define (fact n)
(define (f 0 x) x)
(define (f n x) (f (- n 1) (* x n))) ; 最后一个语句是调用一个函数,此处会进行尾递归优化。
(f n 1))
(fact 1000) ; => 这里不会栈溢出
匹配一个列表
序列可以通过seq
构造,也能通过(<rep1> <rep2> ...)
来匹配。
可以用(... args)
匹配剩余的所有元素,并将其绑定到args
参数。
当剩余参数只是一个symbol的时候,(... args)
可以用...args
替换。
(define (sum-list (head (... tail))) (+ head (sum-list tail)))
(define (sum-list ()) 0)
相同地,如果要将一个列表的值拍平传给一个函数,也可以使用...
。
例如:
(define ls '(1 2 3 4 5))
(+ ...ls) ; 15
(+ (... (map ls string))) ; "12345"
Ranges
范围(Range)代表了一些可迭代的数字,可以用range
或range/inclusive
函数构造。
可以传入构造的区间和步长。
(define 1-5 (range 1 6)) ; [1, 6)
(define 1-5-odds (range 1 6 2)) ; [1, 6),步长为2
(define 1-5 (range/inclusive 1 5))
(define 1-5-odds (range/inclusive 1 5 2))
(list ...1-5) ; (1 2 3 4 5)
副作用
Lisa支持直接改变一个可变变量的值。不同的是,你需要通过define-mutable!
宏声明一个可变变量。
如果当前上下文中存在同名变量,该变量会被初始化为相同的值,否则初始化为()
。
你可以通过set!
宏来改变可变变量的值,但是你不能改变不可变变量的值。
(define (make-counter init)
(define-mutable! init)
(lambda ()
(set! init (+ init 1))
init))
(define c (make-counter 0))
(c); => 1
(c); => 2
(set! c 3) ; set! Error: Can not assign an immutable value c.
宏
定义宏
在Lisa中,定义一个宏在形式上类似于定义一个具名函数。(define-macro (name args*) body*)
。宏的每一个参数按照原本的样子,不经过计算被传入。
宏最后应当返回一个被引用的表达式,该表达式会在被调用的地方求值。
(define-macro (unless predicate consequence alternative)
`'(if ~predicate ~alternative ~consequence))
; It is also possible to define a polymorphic macro using pattern matching.
(define-macro (reversed-apply (f x y))
`'(~f ~y ~x))
(define-macro (reversed-apply (f x))
`'(~f ~x))
(reversed-apply (- 2 3)) ; => (- 3 2) => 1
(reversed-apply (- 2)) ; => -2
正因为宏参数的解析也是通过模式匹配,因此被引用的符号可以直接被视为关键词。
(define-macro (is a 'equals 'to b) `'(= ~a ~b))
(define-macro (is a 'not 'equals 'to b) `'(if (is ~a equals to ~b) false true))
(is 1 equals to 1) ;=> (= 1 1) => true
(is 1 not equals to 2) ;=> (if (is 1 equals to 2) false true) => true
注意,虽然在宏定义中,模式匹配守卫还是可以使用的,但是因为这些表达式没有被计算,因此你不能获取他们真实的值。虽然你可以用eval
来获取计算后的值,但是在宏被展开后可能会有意想不到的重复计算。
定义短语
在Lisa中,你可以定义一些短语。这些短语可以被用作隐式转换或者自定义语法等功能。
(define-phrase (args*) body*)
(define-macro (`&__PHRASE__` args*) body*)
&__PHRASE__
宏会在当前表达式没有合法的结果,并且定义了&__PHRASE__
宏的时候被自动应用。
注意和宏定义不同的是,在新的作用域中使用define-phrase
定义一个新的宏不会覆盖之前的值,相反会创建一个新的分支。
(define-phrase (a '+ b) `'(+ ~a ~b)) ; <==>
(define-macro (`&__PHRASE__` a '+ b) `'(+ ~a ~b))
(3 + 2) ; 会被转换为
(`&__PHRASE__` 3 + 2) ; 因为 3 不能应用到 (+ 2) 上
; 更加通用的版本(使用守卫确保了中缀的操作符是callable的)
(define-phrase (a f b (when (callable? f))) `'(~f ~a ~b))
; callable? 的定义参见 prelude.lisa.
("hello" .startsWith "h") ; => (.startsWith "hello" "h") => true
语法糖
匿名函数字面量
匿名函数可以通过在表达式前面加上 &
来创建。
其中以 #
开头的,后续跟着数字的变量 被视为形参。通过其后面的编号排序。
单独的#
被视为#0
。
&(+ #1 #2) ; 等价于
(lambda (#1 #2) (+ #1 #2))
&(* # #) ; ==>
(lambda (#) (* # #))
&(- #3 #1) ; ==>
(lambda (#1 #3) (- #3 #1)) ; 和Elixir不同的是,Lisa不会检查缺失的编号。
+
。
语法形如 &<func_name>/<arity>
.
&+/2
会被解释成 (lambda (arg0 arg1) (+ arg0 arg1))
,限制元数可以让函数柯里化,因为定参的函数可以通过length
获取元数。
注意 如果不给匿名函数指定形参,他会被解释成常函数,该函数接受任意参数并返回定值。例如: &1
等价于 (lambda ((... _)) 1)
.
Java互操作
在大多数情况下,Lisa可以很处理Scala库定义的数据结构,因为他们会被自动转换为Lisa对象。如果要转换Java的容器,使用from-java
函数。除了使用点开头的变量来访问对象的方法以外,你还可以使用new
来想Clojure
一样调用构造方法。如果你十分想念doto
宏,你可以自己写一个,如:
(define-macro (doto ex (... ops))
(define sym (gen-sym))
(define ops-with-obj
(map ops (lambda ((fn (... args))) (cons fn (cons sym args)))))
`'(let ()
(define ~sym ~ex)
~...ops-with-obj
~sym))
这样,你可以这么写代码:
(define array-list (doto (new ArrayList) (.add 1) (.add 2))) ; WrappedScalaObject[ArrayList]
(define wrapped-list (from-java array-list)) ; WrappedScalaObject[Iterable]
(define lisa-list (iter wrapped-list)) ; '(1 2)
null
Lisa不欢迎null
,但是为了Java却必须使用null,因此lisa提供了一个字面量null
,代表了jvm中null的概念,在lisa代码中不推荐使用,除非在和Java交互。
因为null在lisa中是字面量,你可以根据pattern matching性质,编写拒绝null的函数。
(define (not-null null) (panic! "Argument should not be null."))
(define (not-null x) x)
代码文档
Lisa约定函数的第一行如果是一个字符串字面量,则该字符串就是该函数的文档。
你可以通过help
函数来查看函数的文档。
逻辑编程(实验性功能)
最后
听起来Lisa像这篇文章中的一种实现,其实实际的顺序恰恰相反。作为一门玩具语言,Lisa的性能实际上并不好,直接解释AST,没有字节码的屑语言。但是我好像找到了她的发展方向。
Q&A
为什么这个文章有如此浓重的翻译腔,却没有指出原作者呢?
翻译自己写的东西都能翻译得那么烂确实是我的问题,但是这有指出的必要吗?
我是一个初学者,请问对学习Lisa有什么建议吗?
别学。
计划(坑)
- 中文文档 (本文章)
- 编译成Javascript
- 尾递归优化 (完成,详见#8)
- 编写UT (部分完成 #9)
- Code as Data, Data as Code (#10)
- 变量声明 (PR 10)
- 参数按名调用
- 访问Java静态类属性,方法 (完成,见PR#11)
- 反射调用构造函数
new
(同PR#11) - 给予是否转换参数的控制权限 (在PR#10中,实现了object/module粒度的控制,之后会实现方法甚至参数粒度的控制) 在PR#11中,实现了method粒度的控制。
r{{ changeExtraDisplaySettings({blurMainImage: true}) }}