写给 Java 程序员的 Lice 教程 2

2017/03/11 Lisp

写给 Java 程序员的 Lice 教程

本教程适用于 Lice 2.X

本文大部分内容都是一本正经介绍语言,有少量的不易察觉的卖萌。
—— by 千里冰封

Lice 是什么

这是一门运行在 JVM 上的解释性 Lisp 方言。

下文中,有些地方会把 Lice 称为 Lisp。可能是因为写的比较仓促,见谅。

Lice 有哪些特色

  • 轻量级
  • 熟悉语法只需要五分钟
  • 没有运行时,标准库可定制
  • 解释器是纯 Kotlin 实现,跨到所有 JVM 存在的平台
  • 和 Java 有良好的互操作性
  • 支持动态元编程,比如 undef
  • 动态解释性语言不需要编译,可以代替一些频繁改动的逻辑
  • 支持全角括号,全角分号,全角引号,全角逗号等初学者易混淆的东西的解析
  • 做 DSL 挺好的
  • 没有保留字, if while 都是函数

Lice 的不足之处

  • 解释性语言效率都屎一样
  • 不支持函数式编程(因为把变量和函数区分对待了)

可以看看它长啥样吗

可以。简单的程序:

(print (str-con "Hello World"))

普通的程序:

(load "lice/math.lice")

(print PI) ; It's in lice/math.lice

(while (> 10 (<-> i 0))
  (|> (print i)
   (type i)
   (-> i (+ 1 i))))

复杂的程序:

(for-each
  i
  (.. 1000 1500)
  (thread|> (force|>
    (println i)
    (write-file
      (file (format "%d.html" i))
      (read-url (url (str-con
        "https://vijos.org/p/" i)))))))

支持 UTF-8 的字符:

Lice > ( println 233 666 ) ; UTF-8
233666
666 => java.lang.Integer

Lice > ( println 233 , 666 )
233666
666 => java.lang.Integer

学习 Lice 需要什么基础

这取决于你想做到什么程度。

将 Lice 作为学习 Lisp 所采用的语言

  • 那么你不需要拥有编程基础
  • 但是你需要会用电脑,以及会安装 JRE。

将 Lice 用于平时写一些小玩具用

  • 少量的编程基础
  • 知道啥是函数调用,啥是返回值

将 Lice 作为自己项目的脚本语言

  • 最基本的 Java 基础,会写匿名内部类或者 Lambda 表达式。

参与 Lice 语言的开发

  • 扎实的 Kotlin 基础
  • 函数式编程基础
  • 面对几千行高度压缩过的代码还能硬着头皮去啃的精神
  • 怒怼千里冰封的勇气

参与维护还有机会收到女装照

环境搭建

下载安装 JRE ,并将它添加到环境变量中。在命令行输入以下指令以检查环境安装:

C:\Users\ice1000>java

如果出现大量说明文字那么安装成功。

如果出现'java' 不是内部或外部命令,也不是可运行的程序或批处理文件。那么说明安装失败,再找找教程吧。

下载 Lice

你可以:

通过网页下载

在 GitHub 的 release 界面下载Lice 的交互式解释器

交互式解释器(下文简称 repl)是一个可执行的 jar 文件。

通过源码构建

git clone https://github.com/ice1000/lice.git
cd lice
git gc
echo 讲道理,我也不知道怎么命令行编译,你还是用 IntelliJ IDEA 吧。

首次运行 GUI 的 repl

首先打开 repl ,可以看到一个窗口。这图片是 1.1 版本的,现在的版本可能会在提示字符上有微小的差距,不影响使用,可以无视。

repl

位于最上方的按钮 Clear Screen 用于清空屏幕。

可以看到屏幕上的三行说明信息,以及最下面的一行提示字符:

Lice >

你会发现这个窗口并不能进行编辑。 这是很正常的,因为下面有一个专门用于输入的输入框,用于输入代码。 回车以将输入的代码提交到 repl 中。

你可以在下面的窗口输入以下代码检查 repl 是否正常工作。

(+ 1 1)

输入这一动作应该是很好理解的,但是有些人就是不知道输入是什么意思(这种人按理说早就被孙明琦劝退了),于是我解释一下:

就是按照下图这样输入。我截取了刚才的 repl 窗口的一部分。

input

这是一段基于括号的 S 表达式的代码,其意义等同于 Java 中的

1 + 1

但是,这里有两个区别。

  1. (+ 1 1)是合法的、可以运行的代码。1 + 1只是你 Java 代码中的一部分,不能单独运行。
  2. (+ 1 1)中的+符号是一个函数,它接收多个数值参数,返回他们相加的结果。而1 + 1的+不是函数,是 Java 语言语法的一部分。

可以看到, Lice 是支持使用一些奇奇怪怪的字符作为函数名的。

好了,现在按下回车可以看到你输入的代码(蓝色)、输出(下面的2 => java.lang.Integer),以及新的光标。

output

根据这种格式,下文中的代码都以以下形式提供(就是直接把 repl 里的东西抄进来)。

Lice > (+ 1 1)
2 => java.lang.Integer

也就是说,Lice > 后面的东西就是你该输入的代码。下面是输出。

使用命令行 repl

这一部分不是写给编程新手看的,编程新手看完上面的部分请直接进入下面的“Hello World”部分。

下载 jar ,命令行输入

User > java -jar Lice.jar -cui

你会看到输出:

Lice language repl v2.1
see: https://github.com/ice1000/lice

for help please input: help

Lice > (+ 1 1)
2 => java.lang.Integer

好了废话了半天,我们开始使用 Lice 编程吧。

Hello World

首先我们要像 TCPL 里面说的那样,使用一个输出“Hello World”字符串的程序来开启我们的编程之旅。

Lice > (println "Hello World")
Hello World
Hello World => java.lang.String

好了我们来看一下输出。第一行是这个 println 函数的输出, println 函数接收任意数量的任意类型的参数,输出他们的 toString 后的值。

第二行是整个程序的返回值(在这里,就是 print 函数,因为整个程序只有一个函数的调用),前面是它的 toString ,=>后面是它的类型。类型直接采用 Java 的类型。

我们可以再试试:

Lice > (+ 1 1)
2 => java.lang.Integer

Lice > (println "boy " "next " "door")
boy next door
door => java.lang.String

Lice > (println 4 5 0)
450
0 => java.lang.Integer

Lice > (println 0xFFF)
4095
4095 => java.lang.Integer

这说明:

  • println 函数输出它的参数
  • println 函数原封不动地返回它输出的最后一个内容
  • repl 会打印所有没有用到的值和它的类型

写到这里我发现进度太慢(初中都下课了),下文将会稍微快节奏一些。这里预警一下。

那么这就是 println 函数了,对应的还有 print 函数,可以直接观察它的行为来了解它的工作机制。

Lice > (print 100 200 300)
100200300300 => java.lang.Integer

Lice 的语法

Lice 的语法很简单。

  1. 注释只有一种: 分号开头,直到行末都是注释。 ; 比如这就是一个注释。
  2. 函数调用: (函数名 参数 1 参数 2 …)
  3. 字面量: 100 0xFF 0b1010 011 true false null () 233N “字符串” 以上是 Lice 仅有的几种字面量类型。
  4. 空格和逗号用于区分不同元素(就是函数名啦,字面量啦,下同)。空格和逗号语义完全相同,你可以按照自己喜欢的方式使用。

注意:

  1. 字符串不支持逃逸字符,但是支持在源码中写成跨多行的形式。
  2. 整数字面量支持 2 8 10 16 三种进制, 0b 开头是 2 进制, 0 开头是 8 进制 0x 开头是 16 进制(字母不区分大小写)其余被视为十进。
  3. 整数后面加个 n (模糊大小写),可以变成 BigInteger。BigInteger 和浮点类型互操作会变成 BigDecimal。
  4. 不能直接在括号中写一个值,括号里第一个元素必须是函数名,后面是参数。
  5. 每个函数都有返回值。
  6. 不能(+ 1(+ 1 1)),必须(+ 1 (+ 1 1))。否则1(+会被视为一个单独的词法元素。

语法就这么点。你现在已经学会 Lice 这门语言了,但是你肯定还不知道怎么编程。毕竟 if while 这些结构你都不知道咋整。 其实这些都是标准库函数,将在下文介绍。

你还可以通过 Lice 强(la)大(ji)的动态元编程能力来把他们取消定义掉,这样这语言就没法用了。

基本的标准库函数

数值运算

这部分很简单,就是整数运算。支持加减乘除之类的操作。

Lice > (+ 1 1 1)
3 => java.lang.Integer

Lice > (- 10 5 1)
4 => java.lang.Integer

Lice > (* 2 2 2 2)
16 => java.lang.Integer

Lice > (/ 100 10)
10 => java.lang.Integer

这些函数都可以嵌套:

Lice > (+ 1 1 (* 2 2 (/ 100 10)))
42 => java.lang.Integer

为了解决这个问题可是费劲了我的脑子:

Lice > (- 1N 10)
-9 => java.math.BigInteger

Lice > (- 10N 1)
9 => java.math.BigInteger

Lice > (+ 1 1)
2 => java.lang.Integer

Lice > (+ 9999999999999999999999999999999999999N 9999999999N)
10000000000000000000000000009999999998 => java.math.BigInteger

Lice > (- 9999999999999999999999999999999999999N 9999999999N)
9999999999999999999999999990000000000 => java.math.BigInteger

Lice > (+ 1N 1.0)
2.0 => java.math.BigDecimal

Lice > (* 1 1N)
1 => java.math.BigInteger

Lice > (* 1 10 1290 )
12900 => java.lang.Integer

Lice > (* 129308 130.0)
1.681004E7 => java.lang.Double

Lice > (* 129308 130.0 1N)
1.681004E+7 => java.math.BigDecimal

还有比较函数,返回布尔值,下面是它们的行为实例。它涉及到了一些复杂行为:

Lice > (== 10 10)
true => java.lang.Boolean

Lice > (== 10 10 10 10)
true => java.lang.Boolean

Lice > (== 19 21)
false => java.lang.Boolean

Lice > (< 1 2 3 4)
true => java.lang.Boolean

Lice > (<= 1 1 2 3 100 0xFF)
true => java.lang.Boolean

Lice > (< 1 1 2 3)
false => java.lang.Boolean

Lice > (!= 1 2 3 4)
true => java.lang.Boolean

Lice > (!= 1 2 1 3)
true => java.lang.Boolean

Lice > (!= 1 2 2 3)
false => java.lang.Boolean

布尔运算

我觉得我不需要多说什么。

Lice > (&& true true)
true => java.lang.Boolean

Lice > (&& true false)
false => java.lang.Boolean

Lice > (&& true null)
type mismatch: expected: Boolean, found: java.lang.Object
at line: 1

Lice > (|| false false true)
true => java.lang.Boolean

Lice > (! true)
false => java.lang.Boolean

包含库函数

事实上,在 2.0 版本后,所有内置的、需要解释器开洞的函数我给一股脑放一起了。然后将部分原本是解释器开洞实现的函数放在几个外部文件中了。

加载一个外部文件的话:

Lice > (load "lice/math.lice")
true => java.lang.Boolean

Lice > (load "lice.deep.dark.fantasy")
lice.deep.dark.fantasy not found!
null => java.lang.Object

第二个例子是当你试图加载一个不存在的路径的文件时,解释器的行为。

字符串操作

Lice > (str-con "deep " "dark " "fantasy")
deep dark fantasy => java.lang.String

Lice > (str-con (str-con "My name " "is Van") " I'm an artist")
My name is Van I'm an artist => java.lang.String

str-con 就是连接字符串, string-connect 的缩写。

还有更多的。这里介绍了大量标准库函数,读者不必完全记忆:

Lice > (split "My name is Van, I'm an artist, I'm an performance artist" " ")
[My, name, is, Van,, I'm, an, artist,, I'm, an, performance, artist] => java.util.ArrayList

Lice > (int->oct 100)
0144 => java.lang.String

Lice > (int->hex 100)
0x64 => java.lang.String

Lice > (int->bin 100)
0b1100100 => java.lang.String

Lice > (str->int "02135")
1117 => java.lang.Integer

Lice > (str->int "100")
100 => java.lang.Integer

Lice > (str->int "-0xFFab219")
-268087833 => java.lang.Integer

Lice > (str->int "-0b1010110")
-86 => java.lang.Integer

Lice > (format "My name is %s" "Van")
My name is Van => java.lang.String

Lice > (->chars "Fucking slaves get your ass back here")
[F, u, c, k, i, n, g,  , s, l, a, v, e, s,  , g, e, t,  , y, o, u, r,  , a, s, s,  , b, a, c, k,  , h, e, r, e] => java.util.ArrayList

流程控制语句

你现在终于要学会怎么使用流程控制了。讲道理,这里看完,你就可以使用 Lice 写出稍微复杂一点的代码了。

Lice > (if (== 1 1) (println "Oh my shoulder") (println "Give up? ha?"))
Oh my shoulder
Oh my shoulder => java.lang.String

Lice > (if (!= 1 1) (println "Oh my shoulder") (println "Give up? ha?"))
Give up? ha?
Give up? ha? => java.lang.String

很简单是不是? if 函数需要两个必须提供的参数,第一个是一个布尔值,是条件。第二个是“true 分支”,如果为真那么调用并返回它。 第三个参数是可选的参数,是“else”分支。

上面第一段代码对应的 Java 代码如下:

if (1 == 1) {
    System.out.println("Oh my shoulder");
} else {
    System.out.println("Give up? ha?");
}

while 也很简单,接收两个参数,第一个是条件,第二个是如果条件为真时执行的代码。因为这里还没有涉及到变量的定义,因此我们暂时只能写死循环:

Lice > (while true (print 1))
1
1
1
1
1
1
... 无限循环,省略

还有一个对应的类似 C 系列语言的 switch、Scheme 的 cond、 不过更加类似 Kotlin 的 when 语句的语句: when。

这个语句接受任意数量的参数。如果参数数量是偶数, 那么每两个一组,作为条件和返回语句。如果参数数量是奇数,那么最后一个会被视为是 default 值。

比如(这段代码是直接写进文件的,可以在 repl 里面 load-file 运行):

(def judge-score score (when
  (>= score 100), "full score! congratulations!"
  (>= score 90), "nice job!"
  (>= score 80), "come on! you can do it!"
  (>= score 70), "please study hard!"
  (>= score 60), "fortunately, you passed."
  (>= score 0 ), "you die!"
  "WTF! you must be kidding."))

有了它,你就不需要巨大的 if 嵌套了。

就语法来讲还是很简单的。

什么? for 循环是什么?

引入副作用

有时你需要同时执行多个语句。比如, if 和 while 每个条件分支都只提供了一个参数来提供给你执行代码。如果你需要实现以下 Java 代码:

long a = 2333333L;
long b = new Random(System.currentTimeMillis())
    .nextLong();
if (a > b) {
  System.out.println("I have a mmp");
  System.out.println(" and ");
  System.out.println("I have not idea whether I should say");
}

在这里你需要用到一个这样的函数:

Lice > (|> (println "billy") (type "billy"))
billy
java.lang.String
billy => java.lang.String

|>,看起来很奇怪是不是?这和 Clojure 中的 run 含义完全相同。我给它起这个名字是因为这个符号看起来像各种 IDE 中表示运行的三角形箭头:

run

而且在 Fira Code 字体打开 ligature 的时候可以看到这两个字符的 ligature :

ligature

于是有:

Lice > (|> (print "I have a mmp") (print " and ") (println "I have not idea whether I should say"))
I have a mmp and I have not idea whether I should say
I have not idea whether I should say => java.lang.String

同时,还有两个比较类似的函数,一个是thread|>,传入的所有表达式将会被在一个单独的线程里求值,并返回最后一个。

Lice > (require lice.thread)
true => java.lang.Boolean

Lice > (thread|> (print 1) (print 2) (print 3))
null => java.lang.Object
1
Lice > 
2
3

看到了没?由于求值被放在了另外的线程中, repl 的输出被打乱了。

还有一个是force|>,执行里面所有的代码并在产生 Exception 的时候退出执行,返回最后一次成功求值的结果。 可以用于强制执行一段代码,比如爬取网页的时候配合thread|>使用。

(for-each i
  (.. 1000 1500)
  (thread|> (force|>
    (println i)
    (write-file
      (file (format "%d.html" i))
      (read-url (url (str-con "https://vijos.org/p/" i)))))))

上面的代码可以爬取 vijos 的 1000 到 1500 号题目到当前目录。

那么你可能会问了,上面的->是啥意思啊?

变量

Lice 有两个函数,它们长得非常形象:

  • ->,用于设置全局变量的值
  • <->,用于取出一个值,并提供默认值。如果变量未定义,那么就定义,填入默认值并返回该值。

我们可以直接从 repl 里面看到它的能力。

Lice > (-> name-of-variable "Pachouli Go")
Pachouli Go => java.lang.String

Lice > name-of-variable
Pachouli Go => java.lang.String

Lice > (-> file (file "sample/tests/test2.lice"))
sample\tests\test2.lice => java.io.File

Lice > file
sample\tests\test2.lice => java.io.File

Lice > undefined-name
undefined variable: undefined-name

at line: 1

Lice > (<-> undefined-but-with-default "deep")
deep => java.lang.String

Lice > (<-> name-of-variable "Fucking slaves")
Pachouli Go => java.lang.String

Lice > (<-> undefined-but-with-default "dark")
deep => java.lang.String

讲道理我觉得还是很简单的。

函数

这里就需要用到“局部变量”和“作用域”的概念了。函数定义语法是这样的:

(def function-name param1 param2 body)

解释一下:第一个参数是函数名,最后一个参数是函数体,中间的就是参数表,这样写的话比起 Scheme 就少了一个括号。

比如:

Lice > (def abs a (if (> a 0) a (- 0 a)))
null => java.lang.Object

Lice > (abs 10)
10 => java.lang.Integer

Lice > (abs -10)
10 => java.lang.Integer

然后这里, abs 内部的 a 就是局部变量了,你可以对它赋值,函数内部赋值后对外不可见。

小结

我仔(gen)仔(ben)细(mei)细(you)看了一遍上文,我觉得介绍的差不多了,现在你基本已经可以自由地使用 Lice 编程了。等到啥时候我有闲心了给你们写一篇讲标准库的。

你们也可以参与标准库的维护,而且这个标准库非常不正式,纯粹就是满足一下我对这语言的虚荣心(逃

元编程

讲道理,目前的元编程只有一点点。比如有一个 Ruby 风格的 undef :

Lice > (print "Bye, World!")
Bye, World!
Bye, World! => java.lang.String

Lice > (undef print)
null => java.lang.Object

Lice > (print "give me an error!")
undefined function: print

at line: 1

虽然我感觉没什么卵用,但是拿来作为奇技淫巧玩玩还是可以的。

还有函数别名 alias ,判断函数是否定义的 def?函数等。这些函数的用法可以在仓库的 sample/demos 里面看到。

和 Java 的交互

现在你将看到如何使用 Java 扩展 Lice 语言。由于本文是面向 Java 程序员,我假定你可以看懂下面的代码。

import lice.Lice;
import lice.compiler.model.ValueNode;
import lice.compiler.util.SymbolList;
import java.io.File;

class TheJavaClass {
  public static void main(String[] args) {
    // 创建一个符号表,可以往里面添加方法。
    // 构造函数调用默认的就行了
    SymbolList sl = new SymbolList();
    // 定义一个方法
    sl.defineFunction(
        // 方法的名字
        "java-api-invoking",
        // 方法体,一个 Lambda
        // 参数说明:
        // ln 是这段代码在源码中的行号,类型是 int ,可以在报错的时候给出恰当的信息
        // ls 是 List<Node>,每个 Node 都是传入的参数,按顺序放好了的。
        // 有一个 eval()方法,用于给这个参数进行求值。只要不是循环结构,一般只求值一次。
        // 然后再返回一个 Node
        (meta, ls) -> {
          System.out.println("");
          return new ValueNode(100, meta);
        }
    );
    Lice.run(new File("test.lice"), sl);
  }
}

先给 IntelliJ IDEA 的 Markdown 编辑器点个赞,支持语言的高亮真是帮大忙了:

highlighting languages

首先你需要了解两个类和一个接口。

Value 类

这表示 Lice 语言执行过程中产生的“值”。它有一个 Object 属性和一个 Class<*>属性。

Object 属性是它的值, Class<*>属性是它的类型。

  • 为什么不直接从 obj.getClass() 获取类型?
    为了使 obj 为 null 的时候依旧有效。

Node 接口

这是这个 Lisp 语法树的一个节点的抽象。

  • 它具有一个方法 eval():对这个节点进行求值。
  • 它还具有一个属性:在源码中的行号(用于报错)。

之所以它是接口,是因为它有两种可能的求值过程:

  • 一个表达式:函数调用+参数传递,求值结果是函数返回值
  • 一个字面量:就是这个字面量的值

ValueNode 类

它实现了 Node 接口。你需要传入一个值,随便一个值都可以,比如一个对象。这里以刚才给出的代码为例:

(meta, ls) -> {
  return new ValueNode(100, meta);
}

在定义了这个方法之后,运行指定 Lice 脚本, Lice 代码就可以拿到 100 这个对象了。

比如我们在 Lice 脚本里面写下如下代码:

(print (java-api-invoking))

保存在上面 Java 代码所指定的 test.lice 中,运行上面的 Java 代码,将会得到如下输出:

100

100 => java.lang.Integer

Process finished with exit code 0

也就是说,我们成功将对象传给了 Lice。

类似的,我们还可以在方法里面添加一些可变的对象(我知道这很不 fp ,但是这起码算一种交互啊)

import lice.Lice;
import lice.compiler.model.ValueNode;
import lice.compiler.util.SymbolList;
import java.io.File;

class TheJavaClass2 {
  public static void main(String[] args) {
    SymbolList sl = new SymbolList();
    final int[] a = new int[1];
    a[0] = 0;
    sl.defineFunction(
        "java-api-invoking2",
        (meta, ls) -> {
          System.out.println("Boy next door");
          a[0]++;
          return new ValueNode(a[0], meta);
        }
    );
    Lice.run(new File("test.lice"), sl);
  }
}

再在 test.lice 里面多调用几次这个函数:

(print (java-api-invoking))
(print (java-api-invoking))
(print (java-api-invoking))
(print (java-api-invoking))
(print (java-api-invoking))

运行结果:

1
2 => java.lang.Integer
3
4 => java.lang.Integer
5
6 => java.lang.Integer
7
8 => java.lang.Integer
9
10 => java.lang.Integer

这里给个实际的例子。之前不是说 Lice 不支持逃逸字符吗?如果我需要

"The \"Deep dark fantasy\""

怎么办?

没有逃逸字符我要死了

import lice.Lice;
import lice.compiler.model.ValueNode;
import lice.compiler.util.SymbolList;
import java.io.File;

class TheJavaClass3 {
  public static void main(String[] args) {
    SymbolList sl = new SymbolList();
    final int[] a = new int[1];
    a[0] = 0;
    sl.defineFunction(
        "quote",
        (meta, ls) -> new ValueNode("\"", meta)
    );
    Lice.run(new File("test.lice"), sl);
  }
}

然后在 test.lice 里面:

(println "The " (quote) "deep dark fantasy" (quote) "")

看看效果如何呢?

The "deep dark fantasy"
 => java.lang.String

我说完了

好了本篇教程就到这里啦。


Search

    Post Directory



    如果觉得这篇文章给您带来了收获或者说它值得您这么做,您可以选择对我进行捐助。
    下面是微信支付哟