Roselia-Blog

Lisa Language

The introduction of Lisa.

11/17/2019 14:13:13

见习魔法师

What is Lisa?

Lisa 是用Scala写的一门Lisp方言及其解释器,运行在JVM上。 因为是Lisp的方言,Lisa是动态类型的。 Lisa的语法受到了Lisp和Elixir的启发,和两者有许多相似之处(抄了很多)。 我们知道,一门解释性的语言的性能是很弱的(尤其是动态类型的语言),因此Lisa不是为了性能而生的,她在一开始是作为一门JVM上的胶水语言而设计的,为了能方便调用Scala/Java编写的API。我们知道,一个正常人很难喜欢上用Lisp系语言写工业代码,因此,Lisa不是为了成为一门工业级代码而设计的,甚至不是为了让人类编写代码而设计的。那你猜猜是谁要去编写Lisa代码呢?

本文是Lisa的一个简要文档,如果你有Lisp或Erlang/Elixir的背景的话,你应该能很快理解。

基本类型

复合类型

Seq, List

序列和列表可以通过内建的函数seqlist构造。

(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?来区分他们。

记录类型(Record)

记录类型和Map十分相似。

可以通过 record 函数构造. (record 'a 1 'b 2) => 构建一个记录,类似于{a: 1, b: 2} 记录 sa属性可以通过一下方式访问:

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)
和Elixir一样,分支从定义开始从上往下匹配,因此可能遇到部分分支永远无法匹配的状况,因此Lisa要求先定义边界情况。 而且仅仅定义的函数在同一个作用域(scope)下有同名的函数时,才会生成多态函数,否则会生成全新的函数去覆盖(shadow)。 这样设计是为了防止在函数中定义函数时,因为外部已经定义好的函数的影响从而发生一些意料之外的事情。

特别地,当你想要匹配在上下文中相同的变量时,可以使用反引号

(define x 0)
(define (zero? `x`) true) ; 仅仅匹配`x` (0)
(define (zero? x) false) ; 匹配任意值

Pattern Matching Guard

模式匹配最后一个参数可以是守卫,代表匹配到的参数必须满足的要求。形如(when <predicate>),这里的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存在模式匹配,你不大需要用到ifcond

(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不会检查缺失的编号。
第二种匿名函数字面量是为了限制不定参函数的元数(arity),比如+。 语法形如 &<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)

逻辑编程(实验性功能)

最后

听起来Lisa像这篇文章中的一种实现,其实实际的顺序恰恰相反。作为一门玩具语言,Lisa的性能实际上并不好,直接解释AST,没有字节码的屑语言。但是我好像找到了她的发展方向。

Q&A

为什么这个文章有如此浓重的翻译腔,却没有指出原作者呢?

翻译自己写的东西都能翻译得那么烂确实是我的问题,但是这有指出的必要吗?

我是一个初学者,请问对学习Lisa有什么建议吗?

别学。

计划(坑)