Clojure - Note 2

Part 3 - 1

Note for Clojure 入门教程

概述

Clojure 里的每个操作被实现成以下三种形式中的一种:

  • 函数 (function)
  • 宏 (macro)
  • 特殊表 (special form)

几乎所有的函数和宏都是用 Clojure 实现的,special form 不是用 Clojure 实现的,它是 Clojure 的类似其它语言中的关键字,个数不多,比如 catch, def, do, finally, fn, if, let, loop ...

传统语言如 Java 的方法调用:

methodName(arg1, arg2, arg3);

Clojure 的方法调用:

(function-name arg1 arg2 arg3)

左括号被移到了最前面,逗号和分号不需要了,我们称这种语法叫 form (表?),这种风格简单而又美丽...

对 Clojure 代码的处理分为三个阶段:读入期,编译期以及运行期。在读入期,读入期会读取 Clojure 源代码并且把代码转变成数据结构,基本上来说就是一个包含列表的列表的列表...(所以人们说,Lisp,代码即数据) 在编译期,这些数据结构被转化成 Java 的 bytecode。在运行期这些 Java bytecode 被执行。函数只有在运行期才会执行。而宏在编译期就被展开成实际对应的代码了。

开始吧

如果安装 Clojure,暂略。

Clojure 语法

Lisp 方言有一个非常简法的语法,数据和代码的表达形式是一样的 (S 表达式?),一个列表的列表,很自然地在内存中表达成一个 tree。(a b c) 表示对函数 a 的调用,参数是 b 和 c,如果要表示一个列表 (a b c),则需要使用 '(a b c)(quote (a b c))

语法糖汇总,暂略。

数据类型。

符号 (symbols),是用来给东西命名的,这些名字被限制在名字空间里 ... 没太明白,后面还会再讲到吧。

后面果然又有讲到,其实符号就是类似变量名啦,用 def 来给它绑定值。

user=> (def n 3)
#'user/n
user=> (* n 2)
6

上面的 n 就是一个符号,它被限定在 user 这个名字空间中。

关键字以冒号打头,被用来当作唯一标示符,通常用在 map 中 (如 :red, :green ...)

REPL

REPL (read-eval-print loop)

查看一个函数的帮助,用 (doc _name_)

查看一个函数的源代码,用 (source _name_)

加载并执行一个 clojure 文件,用 (load-file "_file-path_")

变量

实际是指不可变的变量。def / let / binding。

def 定义的是全局的 binding,而且可以用 def 改为同一个变量的值。(咦,不是说不可变的吗? 这怎么又可变啦,奇怪?)

let 定义的 binding 只在当前表达式中有效,具有局部作用域,可以覆盖由 def 定义的全局 binding。

binding 类似 let,只在当前表达式中有效,不同的是,let 的 binding 不具有传递性,即在 let 作用域中调用另一个函数,这个函数无法使用 let 中的 binding,而 binding 定义的变量却可以。

可以看教程提供的例子,这里就略去。

集合

list / vector / set / map。

当然,Clojure 还可以使用 Java 中提供的集合类型,但不推荐这么做,因为 Clojure 自带的集合类型更适合函数式编程。

所有的 Clojure 集合都是不可修改的、异源的以及持久的。

不可修改意味着一旦一个集合产生后,不能从中删除一个元素,也不能添加一个元素。(函数式编程的特征)

异源,意味着在一个集合中放置任何类型的元素,不需要所有元素都具有相同类型。

持久,意味着一个集合新的版本产生后,旧的版本还存在。Clojure 以一种高效的,共享内存的方式来实现这个的。(现代语言好像都是这么做的...)

集合的各种操作函数,如

  • count / reverse
  • map / apply / filter
  • first / rest / last / nth
  • every? / not-every? / some / not-any?

详略。

列表 - list

相当于 Java 中的 LinkedList,链表。

创建 list 的方式:

(def stooges (list "Moe" "Larry" "Curly"))
(def stooges (quote ("Moe" "Larry" "Curly")))
(def stooges '("Moe" "Larry" "Curly"))

相关的函数:

  • some
  • contains?
  • conj / cons
  • remove
  • into
  • peek / pop

详略。

向量 - vector

vector,相当于 Java 中的 ArrayList,数组,可以随机访问。定义函数时的参数列表就是 vector。

创建 vector 的两种方式:

(def stooges (vector "Moe" "Larry" "Curly"))
(def stooges ["Moe" "Larry" "Curly"])

一般情况下,推荐使用 vector 而不是 list。(原因是因为 [] 的语法不容易和 S 表达式混淆? 这原因有点勉强哦)

对 list 操作的函数同样适用于向量,除此之外,还有一些专门用于操作向量的函数:

  • get
  • assoc (相当于 replace 或 push)
  • subvec

详略。

Set

不包含重复元素的集合,有两种 set:排序的和不排序的。

创建 set 的方式:

(def stooges (has-set "Moe" "Larry" "Curly"))    ; not sorted
(def stooges #{"Moe" "Larry" "Curly"})           ; not sorted, same as above
(def stooges (sorted-set "Moe" "Larry" "Curly")) ; sorted

contains? 方法:

(contains? stooges "Moe")  ; -> true
(contains? stooges "Mark") ; -> false

set 集合可以作为函数来使用 (这一点相比传统语言有点神奇):

(stooges "Moe")  ; -> "Moe"
(stooges "Mark") ; -> nil
(println (if (stooges person) "stooge" "regular person"))

conj / into 对 set 同样适用。disj 函数通过去掉 set 中给定的元素来创建新的 set:

(def more-stooges (conj stooges "Shemp"))      ; -> #{"Moe" "Larry" "Curly" "Shemp"}
(def less-stooges (disj more-stooges "Curly")) ; -> #{"Moe" "Larry" "Shemp"}

其它函数,略。

映射 - map

key-value 对,key 和 value 可以是任意对象。

创建 map 的几种方式 (注意,逗号不是必须的,这里只是为了增加可读性):

(def popsicle-map (hash-map :red :cherry, :green :apple, :purple :grape))
(def popsicle-map {:red :cherry, :green :apple, :purple :grape}) ; same as above
(def popsicle-map (sorted-map :red :cherry, :green :apple, :purple :grape))

map 可以作为函数,如果 key 是关键字 (即以冒号开头的值),那么 key 也可以作为函数。(很神奇吧)

三种取得 :green 对应的值的方法:

(get popsicle-map :green)
(popsicle-map :green)
(:green popsicle-map)

contains? 作用于 map 时,它返回 map 是否包含给定的 key。

(contains popsiclie-map :green) ; -> true

keys 得到 map 所有 key 的集合,vals 得到 map 所有 value 的集合。

(keys popsicle-map) ; -> (:red :green :purple)
(vals popsicle-map) ; -> (:cherry :apple :grape)

assoc,在原来 map 的基础上添加新的 key-value 对,创建新的 map。

dissoc,在原来 map 的基础上,忽略一些 key-value 对,创建新的 map。

遍历 map,用 doseq 函数。

(doseq [[color flavor] popsicle-map]
  (println (str "The flavor of" (name color) " popsicles is " (name flavor) ".")))

name 函数的作用是将关键字 (keyword) 转换成字符串。

select-keys,选择 map 中的子集。

conj,将两个 map 合集,得到一个新的 map。

嵌套的 map

(def person {
  :name "Mark Volkmann"
  :address {
    :street "644 Glen Summit"
    :city "St. Charles"
    :state "Missouri"
    :zip 63304}
  :employer {
    :name "Object Computing, Inc."
    :address {
      :street "12140 Woodcrest Executive Drive, Suite 250"
      :city "Creve Coeur"
      :state "Missouri"
      :zip 63141}}})

如何取得内嵌的 key 对应的 value,比如取得 person 的 employer 的 adderss 的 city 的值。用 Java 表示就是 person.employer.address.city。

Clojure 有以下几种方法实现:

(get-in person [:employer :address :city])
(-> person :employer :address :city)
(reduce get person [:employer :address :city])

get-in 和 reduce 是函数,-> 是宏。-> 类似 Elixir 中的 |>,前一个函数的返回值作为后一个函数的参数。

(f1 (f2 (f3 x))
(-> x f3 f2 f1)

(前面说过,如果 map 的 key 是关键字,那么 key 可以作为函数)

clojure.contrib.core 还有一个 -?> 宏,如果链上有任何一个值遇到 nil,它就直接返回,能避免崩溃。

reduce 的解释,暂略。

assoc-in 可以修改一个内嵌的 key 的值。

(assoc-in person [:employer :address :city] "Clayton")

update-in 也是用来修改一个内嵌的 key 的值,但它是通过一个给定的函数来计算得到。

(update-in person [:employer :address :zip] str "-1234") ; -> new zip: 63141-1234

StructMap

类似 map,模拟 Java 中的 JavaBean。它比普通的 map 的优点就是,它把一些常用的字段抽象到一个 map 里面去,这样你就不用一遍一遍的重复了。并且和 Java 类似,他会帮你生成合适的 equals 和 hashCode 方法。并且它还提供方式让你可以创建比普通 map 里面的 hash 查找要快的字段访问方法 (JavaBean 里面的 getXXX 方法)。

(Elixir 中也有结构体的概念。)

create-struct 函数和 defstruct 宏都可以用来定义 StructMap,key 一般是 keyword。

(def vehicle-struct (create-struct :make :model :year :color))
(defstruct vehicle-struct :make :model :year :color)

用 struct 实例化一个 StructMap 对象 (所以,函数式也还是有对象的嘛...),相当于 Java 中的 new。

(def vehicle (struct vehicle-struct "Toyota" "Prius" 2009))

accessor 函数可以创建一个类似 Java 中的 getXXX 方法,它的好处是避免 hash 查找,它比普通的 hash 查找要快。

(def make (accessor vehicle-struct :make))
(make vehicle)  ; -> "Toyota"
(vehicle :make) ; same but slower
(:make vehicle) ; same but slower

函数定义

def / fn / defn。

函数必须先定义,再使用。有时候做不到,比如两个方法相互调用,Clojure 采用了和 C 语言类似的做法,先声明,用 declare。

(declare function-name)

Part 1 和 Part 2 里讲过的了就不再赘述。

函数定义可以包含多个参数列表以及对应的方法体,每个参数列表必须包含不同个数的参数,因为 Clojure 是根据参数个数来进行重载的 (Elixir 也是这样的)。

(defn parting
  ([] (parting "World"))
  ([name] (parting name "en"))
  ([name language]
    (condp = language
      "en" (str "Goodbye," name)
      "es" (str "Adios," name)
      (throw (IllegalArgumentException.
        (str "unsupported language" language))))))

(println (parting)) ; -> Goodbye, World
(println (parting "Mark")) ; -> Goodbye, Mark
(println (parting "Mark" "es")) ; -> Adios, Mark
(println (parting "Mark" "xy")) ; -> java.lang.IllegalArgumentException: unsupported language xy

匿名函数,fn / #(),前者可以包含多个表达式,后者只能有一个表达式。如果有多个参数,可以用 %1 %2 表示,如果只有一个,可以用 % 表示。

(def years [1940 1944 1961 1985 1987])
(filter (fn [year] (even? year)) years) ; -> 1940 1944
(filter #(even? %) years) ; -> same as above

defmulti / defmethod 经常被用在一起来定义 multi-method。

宏 defmulti 的参数有两个,第一个是方法名,第二个是 dispatch 函数,它会作用是方法的参数上,其返回值会被用来选择到底调用哪个被重载的函数。

宏 defmethod 的参数,第一个是方法名,第二个是上面的 dispatch 函数的返回值,剩下的参数是方法的参数以及方法体。

看例子就明白了。

(defmulti what-am-i class)
(defmethod what-am-i Number [arg] (println arg "is a Number"))
(defmethod what-am-i String [arg] (println arg "is a String))
(defmethod what-am-i :default [arg] (println arg "is something else"))

(what-am-i 19)      ; -> 19 is a Number
(what-am-i "Hello") ; Hello is a String
(what-am-i true)    ; true is something else

dispatch 就是一个普通的函数,可以自己定义。

complement 函数,接受一个函数作为参数,如果这个函数的返回值为 true,那它返回 false,反之亦然,相录于取反操作。

comp 把任意多个函数组合成一个,前面一个函数的返回值作为后一个函数的参数。类似 Elixr 的 |>

partial 函数,创建一个新函数,通过给旧的函数制定一个初始参数。Python 有类似用法,偏函数,详略。

Java 互操作

导入 Java 类库,用 import,java.lang 默认已导入。

(import
  '(java.util Calendar GregorianCalendar)
  '(javax.swing JFrame JLabel))

访问类中的常量。

(. java.util.Calendar APRIL)
(. Calendar APRIL)
java.util.Calendar/APRIL
Calendar/APRIL

调用类中的静态方法。

(. Math pow 2 4)
(Math/pow 2 4)

创建 Java 对象,两种方式。

(import '(java.util Calendar GregorianCalendar))
(def calendar (new GregorianCalendar 2008 Calendar/APRIL 16)) ; April 16, 2008
(def calendar (GregorianCalendar. 2008 Calendar/APRIL 16))

访问对象方法,两种方式。

(. calendar add Calendar/MONTH 2)
(. calendar get Calendar/MONTH) ; -> 5
(.add calendar Calendar/MONTH 2)
(.get calendar Calendar/MONTH)  ; -> 7

推荐使用 .add .get 的方式。前面用点号的方式在定义宏中用得比较多。

可以用 .. 宏实现链式调用。

(. (. calendar getTimeZone) getDisplayName)
(.. calendar getTimeZone getDisplayName)

还有一个类似的宏 .?.,它的区别在于,如果遇到 nil,则立即返回,不再调用,避免出现 NullPointException。

doto 函数可以用来调用一个对象上的多个方法,返回第一个参数,即对象。

(doto calendar
  (.set Calendar/YEAR 1981)
  (.set Calendar/MONTH Calendar/AUGUST)
  (.set Calendar/DATE 1))
(def formatter (java.text.DateFormat/getDateInstance))
(.format formatter (.getTime calendar)) ; "Aug 1, 1981"

memfn 宏,没太明白,先跳过。

代理

说得很简单,先略过。

线程

所有的 Clojure 方法都实现了 java.lang.Runnable 和 java.util.concurrent.Callable 接口,这使得非常容易把 Clojure 函数和 Java 线程一起使用。

(defn delayed-print [ms text]
  (Thread/sleep ms)
  (println text))

(.start (Thread. #(delayed-print 1000 ", World!")))
(print "Hello")

异常处理

try / catch / finally / throw,和 Java 类似,详略。

条件处理

  • if (special form)
  • when / when-not (宏)
  • if-let / when-let (宏)
  • condp (宏)
  • cond (宏)

if 接受三个参数,第一个是条件,第二个是条件为 true 时要执行的表达式,第三个为 false 时执行的表达式,如果表达式有多个,需要用 do 包围。

when / when-not 类似 if,但 when 只支持条件为 true 时要执行的表达式,when-not 只支持条件为 false 时要执行的表达式,如果表达式有多个,不需要用 do 包围。

if-let 把一个值 bind 到一个变量,然后根据这个 binding 的值来决定执行哪个表达式。

(defn process-next [waiting-line]
  (if-let [name (first waiting-line)]
    (println name "is next")
    (println "no waiting")))

(process-next '("Jeremy", "Amanda" "Tami")) ; -> Jeremy is next
(process-next '()) ; -> no waiting

when-let 类似 if-let,但是没有 else 分支,且支持任意多表达式。

condp 相当于 switch / case。condp 接受两个参数,一个是谓语参数,一般是 =,或 instance?。

(condp = value
  1 "one"
  2 "two"
  3 "three"
  (str "unexpected value, \"" value "\""))

(condp instance? value
  Number (* value 2)
  String (* (count value) 2))

cond 宏接受任意多个 谓语/结果 表达式的组合,一旦有匹配上的,则执行相应的表达式。(Elixir 中有相同的用法)

(cond
  (instance? String temperature) "invalid temperature"
  (<= temperature 0) "freezing"
  (>= temperature 100) "boiling"
  true "neither")

相当于 if...else if...else if...else

results matching ""

    No results matching ""