Lisa 是用Scala写的一门Lisp方言及其解释器,运行在JVM上。 因为是Lisp的方言,Lisa是动态类型的。 Lisa的语法受到了Lisp和Elixir的启发,和两者有许多相似之处(抄了很多)。 我们知道,一门解释性的语言的性能是很弱的(尤其是动态类型的语言),因此Lisa不是为了性能而生的,她在一开始是作为一门JVM上的胶水语言而设计的,为了能方便调用Scala/Java编写的API。我们知道,一个正常人很难喜欢上用Lisp系语言写工业代码,因此,Lisa不是为了成为一门工业级代码而设计的,甚至不是为了让人类编写代码而设计的。那你猜猜是谁要去编写Lisa代码呢?
本文是Lisa的一个简要文档,如果你有Lisp或Erlang/Elixir的背景的话,你应该能很快理解。
序列和列表可以通过内建的函数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?
来区分他们。
记录类型和Map
十分相似。
可以通过 record
函数构造.
(record 'a 1 'b 2)
=> 构建一个记录,类似于{a: 1, b: 2}
记录 s
的a
属性可以通过一下方式访问:
get函数可以用以下方式调用:
(get map key)
(get map key not-found)
和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) ; 匹配任意值
模式匹配最后一个参数可以是守卫,代表匹配到的参数必须满足的要求。形如(when <predicate>)
,这里的when
可以是:
when
代表这个表达式必须成功被执行,否则返回失败。when?
表示如果该表达式无法执行,就忽略这个分支,往下面定义的同名函数中寻找匹配。when?
在标准库中常被用来判断类型(如果一个对象满足某种约束,他就属于那个类型)。
(define (fact 0) 1) ; Nothing interesting.
(define (fact n (? (> 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
构造,也能通过(seq <rep1> <rep2> ...)
来匹配。
可以用(... args)
匹配剩余的所有元素。
(define (sum-list (seq head (... tail))) (+ head (sum-list tail)))
(define (sum-list (seq)) 0)
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)
.
在大多数情况下,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)
听起来Lisa像这篇文章中的一种实现,其实实际的顺序恰恰相反。作为一门玩具语言,Lisa的性能实际上并不好,直接解释AST,没有字节码的屑语言。但是我好像找到了她的发展方向。
为什么这个文章有如此浓重的翻译腔,却没有指出原作者呢?
翻译自己写的东西都能翻译得那么烂确实是我的问题,但是这有指出的必要吗?
我是一个初学者,请问对学习Lisa有什么建议吗?
别学。