Clojure - Note 1

References:

函数式编程的特点:

  • 函数作为一等公民,且应该是纯函数
  • 函数可以作为参数传递,也可以作为函数的返回值,高阶函数
  • 数据的不可修改性

Clojure 是一个运行在 JVM 之上的函数式编程语言。它可以访问丰富已存在的 Java 类库,比如文件 IO,多线程,数据库操作,GUI 编程,Web 编程...

ClojureScript 项目可以把 Clojure 代码编译成 JavaScript 代码。

Part 1

Note for Clojure 快餐教程 (1) - 运行在 JVM 上的 Lisp 方言

Clojure 对 Lisp 的改进

  • 可以无缝调用 Java API
  • 针对 Lisp 括号多的问题,Clojure 除了 S 表达式常用的 () 之外,增加了 []{} 两种括号

() 表示列表,[] 表示向量 (即可随机访问的数组),{} 表示 HashMap,#{} 表示 Set。

查看帮助

使用 clojure.repl/doc 宏。

user=> (doc doc)
-------------------------
clojure.repl/doc
([name])
Macro
  Prints documentation for a var or special form given its name

列表

在 Lisp 中,列表这种数据和代码的形式是一样的,都是 S 表达式。S 表达式可以理解成用 () 包围起来的,以第一个符号进行求值的表达式。

所以,我们不能直接用类似 (1 2 3) 这样的表达式来得到一个列表,因为这样一个表达式,会用 1 作为符号或函数,对参数 2 和 3 进行求值,很明显会失败。

所以,有两种方法来得到列表:

  • 用 list 函数来生成列表
  • 用 quote 特殊表 (special form) 来避免被求值,即得到一个未被求值的表

注意,list 是函数,但 quote 不是函数,它也不是宏,而是特殊表,special form,像后面会讲到的 def,fn,也是 special form,我觉得可以理解成其它语言中的关键字,由语言本身提供,不可自己定义,而函数和宏是可以自己再定义的。

quote 还有简略写法,如 quote (1 2 3) 可以简写为 '(1 2 3),可以理解成语言本身提供的语法糖。

user=> (list 1 2 3)
(1 2 3)
user=> (doc list)
-------------------------
clojure.core/list
([& items])
  Creates a new list containing the items.
nil
user=> (quote (2 3 4))
(2 3 4)
user=> (doc quote)
-------------------------
quote
  (quote form)
Special Form
  Yields the unevaluated form.

  Please see http://clojure.org/special_forms#quote
nil
user=> '(3 4 5)
(3 4 5)

quote 对任意 S 表达式,不仅仅是列表,都直接返回原值,使其不被求值,比如:

user=> (quote (+ 1 (+ 2 3)))
(+ 1 (+ 2 3))

列表操作

Lisp 中最常用的 car 和 cdr 在 Clojure 中不被支持。取而代之的是 first / rest / last 之类的函数。

用 first 取得表头:

user=> (first (list 1 2 3 4))
1

用 rest 取得除表头余下的部分:

user=> (rest (list 1 2 3 4))
(2 3 4)

用 last 取得列表最后一个元素:

user=> (last (list 1 2 3 4))
4

用 nth 取得第 n 个元素:

user=> (nth (list 2 3 4) 2)
4

将 first 和 rest 元素组合成一个列表:

user=> (cons 1 '(2 3 4))
(1 2 3 4)

向量

向量类似数组,对于随机访问进行了优化。

Clojure 用 [] 表示向量。由于 [] 不会和 S 表达式冲突,所以就不用像生成列表那样需要显式地用 list 函数或 quote,直接用类似 [1 2 3] 的表达式就可以生成一个向量。

first / rest / last / nth 函数对向量仍然适用。

但需要注意的是,rest 作用在向量上,返回值并不是一个向量,而是一个序列类型 (那是列表吗?)。

user=> (rest [1 2 3 4])
(4 5 6) ;看着像是列表

user=> (class (rest '(1 2 3)))
clojure.lang.PersistentList
user=> (class (rest [1 2 3 4]))
clojure.lang.PersistentVector$ChunkedSeq

哈希表

{} 表示哈希表,使用 key 的名字作为函数名来取得 key 对应的 value。

{:name "hello", :age 18}

user=> (:name {:name "hello", :age 18})
"hello"

集合 (Set)

大中小括号都用完了,只好在大括号前加个 # 表示集合 (Set)。

user=> #{1 2 3}
#{1 3 2} ;为什么不是 #{1 2 3},待验证

集合 (Set) 中的元素不能重复。可以用 sorted-set 函数构建有序的 set。

user=> (sorted-set 1 2 2 3)
#{1 2 3}

变量绑定

相当于其它语言中用 = 进行赋值。在 Clojure 中用 def 特殊表 (即其它语言中的关键字) 来将值绑定到一个全局变量上,也可以将匿名函数绑定到全局变量上。

user=> (def a '(1 2 3))
#'user/a

定义函数

一般用 fn 特殊表来定义一个匿名函数,然后用 def 特殊表来将这个匿名函数绑定到一个变量上。

或者直接用 defn 宏 (我觉得 defn 内部应该就是用 fn + def 来实现的) 来定义函数。

定义一个求平方的匿名函数:

user=> (fn [i] (* i i))
#object[user$eval17$fn__18 0x7f284218 "user$eval17$fn__18@7f284218"]

将其用 def 绑定到一个变量上:

user=> (def sqr (fn [i] (* i i)))
#'user/sqr
user=> (sqr 4)
16

直接用 defn 宏定义函数:

user=> (defn sqr3 [i] (* i i i))
#'user/sqr3
user=> (sqr3 4)
64

通过 defn 宏,还可以为函数提供说明文档,然后通过 doc 函数就可以查看这个说明文档。

user=> (defn inc "return inc x" [x] (+ x 1))
#'user/inc
user=> (doc inc)
----------------------
user/inc
([x])
  return inc x
nil

Part 2

Note for Clojure 小教程

Clojure 简易语法。

注释

Clojure 的注释用 ; (有点特别,既不是 //,也不是 # ...)

; This is a comment
(map #(+ % 3) [2 3 7])

基本类型

  • 布尔值
  • 整型
  • 浮点
  • 字符 (\a \A \u2014 ...)
  • 字符串
  • 符号
  • 关键字 (冒号打头,比如 :red)

集合类型

  • list --> (1 2 3)
  • vector --> [1 2 3]
  • map --> {"key1" 1, "key2" 2}
  • set --> #{1 2 3 4}

函数定义

fn / def / defn,在 Part 1 里讲过的略。

函数定义支持变长参数,定义形式,示例:

(fn mul [& x] #{x})

& 后的参数名为变长参数名,所有参数将作为一个 list 传入函数定义中。比如:

user=> ((fn mul [& x] #{x}) 1 2 3)
#{(1 2 3)}

fn 的语法糖简写形式 #(),同时,可以用 %1 %2 代替参数名。(从后面来看,貌似如果只有一个参数,可以直接用 % 来表示这个唯一的参数)

user=> ((#(+ %1 %2) 2 3))
5

块序列

使用 do 将一些操作序列组成一个块,只有最后一个元素的值返回。

user=> (do 1 (+ 1 2) (- 9 2) 3)
3

设定全局不变量

使用 (let [] ()) 表达式。

user=> (let [x 10 y 100] (do (println x) (println y)))

if / when

(if (cond) (then-clause) (else-clause))

(when (cond) (sta1) (sta2) (sta3))

递归与循环

讲得不是很清楚,略。

quote / unquote

讲得不是很清楚,略。

与 Java 互操作

静态域与静态方法

语法:ClassName/static_method_or_field

示例:

user=> (println Math/PI)
user=> (println (Math/sqrt 9))

创建实例对象

语法:(ClassName.)(new ClassName)

示例:

user=> (java.util.HashMap. ["x" 1 "y" 2])
user=> (new java.util.HashMap ["x" 1 "y" 2])

调用对象的实例方法或域

语法:(.method_or_filed)

示例:

user=> (.divide (java.math.BigDecimal. "42") 2M)
user=> (.x (java.awt.Point. 10 0))
10

设置实例属性

(如果没有提供 setter,可以用 set!)

(注意,操作 Java 对象不属于函数式编程,因为用 set 改变对象属性的值,明显就违返了函数式的原则 - 变量不可变)

示例:

(def point (java.awt.Point 10 10))
(defn setX [po xval] (set! (.x po) xval))
(setX point 15)
(.x point) ;15

链式调用

使用 .. 宏来支持链式调用。

比如,Java 代码是这样的:new java.util.Date().toString().endsWith("2016"),用 Clojure 可以这样表示:

(.. (java.util.Date) toString (endsWith "2016"))

doto 宏,设置对象的多个实例域

Java 代码:

Date time = new Date();
time.setYear(1);
time.setMonth(2);
time.setDate(3);

Clojure 代码:

(doto (java.util.Date.) (.setYear 1) (.setMonth 2) (.setDate 3))

异常处理

抛出异常:

(throw (Exception. "error happens"))

捕捉异常:

(defn throw-catch [f]
  (try (f) (catch ArithmeticException e "No dividing by zero!")
           (catch Exception e (str "You are so bad " (.getMessage e)))
           (finally (println "returning... "))))
(throw-catch #(/ 10 2))

命名空间

创建命名空间

使用 ns (函数,or 宏,or 特殊表?)

(ns my.clojure)

引用其它 Clojure 命名空间

使用 :require 关键字

(ns my.core (:require my.clojure))

只引用其它命名空间的部分函数

使用 :use

(ns my.util (:use [my.core :only [myfunc1 myfunc2]]))    ; 只导入 myfunc1 与 myfunc2
(ns my.util (:use [my.core :exclude [myfunc1 myfunc2]])) ; 只有 myfunc1 与 myfunc2 不导入

导入 Java 类库

使用 import

(import java.util.*)

results matching ""

    No results matching ""