01.环境搭建

Clojure 是什么?

Clojure 是一门运行在jvm 环境下的**Lisp 方言。**笔者一直久仰 Lisp 的大名,最近终于克服了对层层括号的恐惧,准备一睹 Lisp 的芳容啦!实际上手体验之后感觉 Lisp 系语言写起来还是蛮爽的,最重要的是 REPL 实在是太方便了,有了想法之后就可以立刻付诸实践,不用再专门写一个 main 函数或是单元测试来进行验证,直接用 REPL 交互式的进行实验即可。

img

运行环境搭建

由于 Clojure 是运行在 jvm 上的语言,自然需要先配置好 java 运行环境。之后我们需要安装一个 Clojure 依赖管理工具Leiningen(以下简称 lein),安装完毕后别忘了将 bin 目录添加到 PATH 环境变量中。

现在让我们打开 cmd,执行下面这条命令来查看 lein 是否安装正确:

lein version

如果 cmd 中显示这么一行信息,就表示 lein 已经正确安装了。

Leiningen 2.8.1 on Java 1.8.0_144 Java HotSpot(TM) 64-Bit Server VM

使用 REPL

所谓的 REPL 就是读取-求值-打印-循环,当我们在 REPL 中输入一条表达式之后,表达式的结果会立即回显到 cmd 中,然后等待下一条输入。类似 Python 等很多脚本语言都带有 REPL,Clojure 自然也不例外。

在 cmd 中执行下面这条命令就能打开 Clojure 的 REPL 了:

lein repl

之后会在 cmd 中输出下列信息:

nREPL server started on port 49928 on host 127.0.0.1 - nrepl://127.0.0.1:49928
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.8.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0_144-b01
 Docs: (doc function-name-here)
 (find-doc "part-of-name-here")
 Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
 Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=>

我们试着在 REPL 中运行一个 Hello World 程序:

user=> (println "Hello World!")
;; Hello World!
;=> nil

user=>表示我们目前处于 user 命名空间当中。user 命名空间是 REPL 环境的默认命名空间。之后如无必要将会在例程中省略 user=>提示符。

Lisp 系语言以分号(;)表示注释,相当于 C 语言中的(//)。我会用双分号注释(;;)表示命令行输出,箭头注释(;=>)表示表达式的值。

现在让我们把重点放到程序本身:

(println Hello World!)

在 Lisp 中,一对括号表示一个列表结构,其中列表项以空格进行分隔(注:在 Clojure 中,逗号(,)和空格是等价的,因此也可以像其他语言一样用逗号进行分隔)。不同于其他语言的数组,Lisp 列表再被求值时,会被解释成一个函数调用。列表的第一项会被解释成函数名,其余则是函数的参数。

至于 Lisp 为何要这么做?那可就说来话长了,现阶段我们只需要简单的记下即可。

让我们再看一个简单的例子,使用 Clojure 计算 1+1:

(+ 1 1)
;=> 2

没错,在 Clojure 中,(+)也是一个函数,因此它看起来是前缀形式的。这看起来比较别扭,不过好处是我们不再需要考虑运算符优先级的问题了:

(* 3 (+ 1 2))
;=> 9

另一个优势是,前缀形式的(+)函数可以很自然的接受任意数量的参数:

(+ 1 2 3 4 5)
;=> 15

因此(+)函数很自然的能起到列表求和的作用:

(apply + [1 2 3 4 5])
;=> 15

apply 函数的作用是将一个列表中的元素作为函数参数传递给一个函数。

文档查询

REPL 还提供了非常方便的文档查询功能。

  • 使用 doc 宏查询文档

我们可以用 doc 来查看一下+函数的文档:

(doc +)
;;-------------------------
;;clojure.core/+
;;([] [x] [x y] [x y & more])
;;  Returns the sum of nums. (+) returns 0. Does not auto-promote
;;  longs, will throw on overflow. See also: +'
;=> nil

在阅读 Clojure 代码时,如果遇到没见过的函数或宏,马上打开 REPL 查询一下吧!

  • 使用 find-doc 寻找文档

我们试着来求一下 1 和 0 的异或:

(xor 1 0)
;=> CompilerException java.lang.RuntimeException: Unable to resolve symbol: xor in this context, compiling:(null:1:1)

哎呀!Clojure 似乎没有一个叫 xor 的函数。不用担心,我们试试用 find-doc 来查找一下相关函数吧:

(find-doc "xor")
;;-------------------------
;;clojure.core/bit-xor
;;([x y] [x y & more])
;;  Bitwise exclusive or
;=> nil

原来函数名是 bit-xor!让我们来尝试一下:

(bit-xor 0 1)
;=> 1
  • 使用 javadoc 查询 java 类库的文档:
(javadoc java.util.ArrayList)

REPL 会为我们打开一个浏览器页面,显示 ArrayList 的文档。

使用 Lein 新建 Clojure 项目

使用 Lein 创建 Clojure 项目也非常简单,我们先在 cmd 中 cd 到项目要保存的目录下,然后执行这个命令即可:

lein new learning-clojure

这样我们就创建了一个名为 learning-clojure 的项目:

img

项目的目录结构有点像 maven,包含一个包含源文件的 src 文件夹,和一个包含测试文件的 test 文件夹。

使用 REPL 对项目进行测试

后缀名为.clj 的文件就是 Clojure 的源文件了。让我们来看一下 src/learning_clojure/core.clj 里面都有些什么内容吧:

(ns learning-clojure.core)    ; 声明命名空间

(defn foo ; 定义函数foo
 "I don't do a whole lot."    ; 这是foo函数的说明文档
 [x]                          ; 这是foo函数的参数列表
 (println x "Hello, World!")) ; 这是foo函数的函数体

上面的代码定义了一个简单的函数 foo。让我们试着使用 REPL 来测试一下这段代码,首先需要在 cmd 中 cd 到项目的根目录下,然后运行 lein repl 启动 REPL。

接下来我们使用 use 指令将 learning-clojure.core 命名空间中的内容导入到当前命名空间:

(use learning-clojure.core) ; <-注意此处的单引号

为什么这里要加上一个单引号呢?这是因为当 Lisp 对列表进行求值时,会先对列表的每一项进行求值。然而,在我们引入命名空间之前,learning-clojure.core 还不存在,对它求值会发生错误。而单引号(读作 quote)的作用就是阻止对其进行求值:

(a b c) ; 标识符a、b、c还不存在,无法求值
;=> CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context, compiling:(null:1:1)
(a b c) ; 加上单引号之后会返回列表本身
;=> (a b c)
(quote (a b c)) ; 单引号实际上是quote宏的简写形式
;=> (a b c)

引入命名空间之后我们就能使用 REPL 对 foo 函数进行测试了:

(foo "Bar")
;; Bar Hello, World!
;=> nil

现在让我们在 core.clj 文件中添加一个 hello 函数:

(defn hello
 "Say hello to someone."
 [name]
 (str "Hello, " name))

保存修改之后,我们需要在 REPL 中使用:reload 命令重新加载模块

(use :reload learning-clojure.core)
(hello World!)
;=> Hello, World!
下一页