写给 Java 程序员的 Lice 教程

by

写给 Java 程序员的 Lice 教程

本文仅适用于 Lice 1.X

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

(第一次写这么长的技术文章啊)

Lice 是什么

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

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

Lice 有哪些特色

可以看看它长啥样吗

可以。简单的程序:

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

普通的程序:

(require lice.math)

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

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

复杂的程序:

(require lice.io)
(require lice.str)

(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))))))))

学习 Lice 需要什么基础

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

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

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

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

参与 Lice 语言的开发

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

环境搭建

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

C:\Users\ice1000>java

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

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

下载 Lice

你可以:

通过网页下载

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

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

通过源码构建

git clone https://github.com/ice1000/lice.git
cd lice
git gc

讲道理,我也不知道怎么命令行编译,你还是用 IntelliJ IDEA 吧。

首次运行 GUI 的 repl

首先打开 repl ,可以看到一个窗口。

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 v1.1-SNAPSHOT
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 > (print "Hello World")
Hello World
Hello World => java.lang.String

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

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

我们可以再试试:

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

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

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

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

这说明:

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

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

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

Lice 的语法

Lice 的语法很简单。

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

注意:

  1. 字符串不支持逃逸字符,但是支持在源码中写成跨多行的形式。
  2. 整数字面量支持 2 8 10 16 三种进制, 0b 开头是 2 进制, 0 开头是 8 进制 0x 开头是 16 进制(字母不区分大小写)其余被视为十进制。
  3. 整数后面加个 n (模糊大小写),可以变成 BigInteger
  4. 不能直接在括号中写一个值,括号里第一个元素必须是函数名,后面是参数。
  5. 每个函数都有返回值。
  6. Lice 没有变量的概念,使用一个全局符号表取而代之,你可以理解为是只支持全局变量。缺点是无法构建大型程序,优点是实现极其轻量级。
  7. 不能(+ 1(+ 1 1)),必须(+ 1 (+ 1 1))。否则1(+会被视为一个单独的词法元素。
  8. 这是一个 bug ,代码文件第一个字符是分号的话这个字符会被视为函数而不是注释。我认为无可厚非于是没去管,毕竟你只需要不在第一行写注释就行了。

语法就这么点。你现在已经学会 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 (int->double 1))
2.0 => java.math.BigDecimal

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

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

Lice > (* 129308 (int->double 130))
1.681004E7 => java.lang.Double

Lice > (* 129308 (int->double 130) 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

包含库函数

为了防止符号表变得太大,我把常用函数分开放置,简单模块化了一下。 使用一个模块的话需要这样包含它。

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

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

第二个例子是当你试图包含一个不存在的库时,解释器的行为。

字符串操作

字符串操作的大部分函数都在 lice.str 里面。使用它们的话首先需要包含这个函数库。

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

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) (print "Oh my shoulder") (print "Give up? ha?"))
Oh my shoulder
Oh my shoulder => java.lang.String

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

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

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

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

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

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

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

什么? for 循环是什么?

引入副作用

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

if (true) {
  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 > (|> (println "I have a mmp") (println " 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|>使用。

(require lice.io)
(require lice.str)

(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 是不支持变量的(因为我不知道符号表怎么做成栈式的,我菜爆了),但是使用了另一种方法作为替代方案。

这个方案很挫,挫到爆,但是还是有那么一点点用处的。

Lice 有一个全局的字符串到值的映射(就是一个 HashMap ),你可以通过符号来访问它的值。

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

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

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

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

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

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)
null => java.lang.Object

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

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

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

先给个列表操作的代码:

(require lice.thread)

; run this in the command line repl.

(for-each
  i
  (.. 1 10)
  (|>
    (println (<- i))
    (sleep (* 100 (<- i)))
    (print "I'm alive")))

元编程

讲道理,目前的元编程只有一点点。比如有一个 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

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

和 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
        (ln, ls) -> {
          System.out.println("");
          return new ValueNode(100, ln);
        }
    );
    Lice.run(new File("test.lice"), sl);
  }
}

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

highlighting languages

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

Value 类

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

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

Node 接口

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

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

ValueNode 类

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

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

在定义了这个方法之后,运行指定 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",
        (ln, ls) -> {
          System.out.println("Boy next door");
          a[0]++;
          return new ValueNode(a[0], ln);
        }
    );
    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

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


Tweet this
Top


创建一个 issue 以申请评论
Create an issue to apply for commentary


协议/License

本作品 写给 Java 程序员的 Lice 教程 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,基于 http://ice1000.org/2017/03/01/Intro2Lice/ 上的作品创作。
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
知识共享许可协议