Clojure学习笔记(一):数据结构

最近学习了Clojure,好记性不如烂笔头,把一些知识点记录了下来。原本想放在一篇文章里的,谁知太长了,只好分成了三篇,本文是第一篇。由于是笔记,比较杂乱,建议阅读前先系统地学习Clojure(比如 Clojure - Function Programming for JVM),然后用本笔记知识梳理。

概述

Clojure是一个动态类型的,运行在JVM(JDK5.0以上),并且可以和java代码互操作的函数式语言。这个语言的主要目标之一是使得编写一个有多个线程并发访问数据的程序变得简单。

Clojure的发音和单词closure是一样的。Clojure之父是这样解释Clojure名字来历的

“我想把这就几个元素包含在里面: C (C#), L (Lisp) and J (Java). 所以我想到了 Clojure, 而且从这个名字还能想到closure;它的域名又没有被占用;而且对于搜索引擎来说也是个很不错的关键词,所以就有了它了.”

基本类型

Boolean

true, false

常用函数:

  1. not
  2. and
  3. or

Nil

nil,只有false与nil会被计算为false,其它的都为true

常用函数:

  1. nil?

Character

字符的表示要在前面加反斜杠 \a \b \c

Number

数字1, 2

常用函数:

  1. +, -, *
  2. /(分数形式),quot(商),rem(余数)
  3. inc,dec
  4. min, max
  5. =,<,<,>,>=
  6. zero?,pos?,neg?,number?

String

字符串,如 “hello”

常用函数

  1. str: 拼接多个字符串
  2. subs: 子字符串(0为开始下标)
  3. string?

KeyWord

关键字,常用于Map中的key,:tag:doc

Symbol

变量名,一个命名空间中唯一。如(def x 1),就申请了一个'user/x的Symbol。

集合

list, vector, set, map

clojure中的任何变量都是不变的,当对一个变量进行修改时,都会产生一个新的变量(clojure会使用共享内存的方式,只会消耗很小的内存代价)。有点像scala中的val哦。

  • count
    返回集合里面的元素个数

    (count [19 "yellow" true]) ; -> 3
  • reverse
    把集合里面的元素反转

    (reverse [1 2 3]) ; -> (3 2 1)
  • map
    对一个集合内的每个元素都调用一个指定的方法。每个元素调用方法后的返回值再构成一个新的集合返回。

    ; 使用匿名函数对每个元素加3
    (map #(+ % 3) [2 4 7]) ; -> (5 7 10)
    ; 函数可以有多个参数,则指定多个集合,执行次数取决于个数最少的集合长度
    (map + [2 4 7] [5 6] [1 2 3 4]) ; -> (8 12)
  • apply
    把集合里的所有元素都作为函数参数做一次调用,并返回这个函数的返回值。

    (apply + [2 4 7]) ; -> 13
  • conj & cons
    conj (conjoin), 添加一个元素到集合里面去。如果是list类型,则新元素插到队头;如果是vector类型,则新元素插到队尾。

    cons (construct),也是添加一个元素到集合里面去。只不过新元素都会在队头。返回类型都会变成seq。

    ; cons用以向列表或向量的起始位置添加元素
    (cons 4 [1 2 3]) ; => (4 1 2 3)
    (cons 4 '(1 2 3)) ; => (4 1 2 3)
    ; conj将以最高效的方式向集合中添加元素。
    ; 对于列表,数据会在起始位置插入,而对于向量,则在末尾位置插入。
    ; 注意:第一个参数是集合,第二个是要插入的元素,可以有多个元素
    (conj [1 2 3] 4) ; -> [1 2 3 4]
    (conj [1 2 3] 4 5) ; -> [1 2 3 4 5]
    (conj '(1 2 3) 4 5 6) ; -> (6 5 4 1 2 3)

常用的集合操作

;从集合中取一个元素
(def stooges ["Moe" "Larry" "Curly" "Shemp"]) (first stooges) ; -> "Moe"
(second stooges) ; -> "Larry"
(last stooges) ; -> "Shemp"
;从集合中取多个元素
(nth stooges 2) ; indexes start at 0 -> "Curly" (next stooges) ; -> ("Larry" "Curly" "Shemp") (butlast stooges) ; -> ("Moe" "Larry" "Curly") (drop-last 2 stooges) ; -> ("Moe" "Larry")
; 得到包含多于3个字符的名字.
(filter #(> (count %) 3) stooges) ; -> ("Larry" "Curly" "Shemp")
(nthnext stooges 2) ; -> ("Curly" "Shemp")
;一些谓词操作
(every? #(instance? String %) stooges) ; -> true
(not-every? #(instance? String %) stooges) ; -> false
(some #(instance? Number %) stooges) ; -> nil
(not-any? #(instance? Number %) stooges) ; -> true

Lists

Lists 相当于Java中的LinkedList。

  • 创建

    (def stooges (list "Moe" "Larry" "Curly"))
    (def stooges (quote ("Moe" "Larry" "Curly")))
    (def stooges '("Moe" "Larry" "Curly"))
  • some
    检测一个集合中是否包含某个元素,需要跟一个谓词函数和一个集合。

    (some #(= % "Moe") stooges) ; -> true
    (some #(= % "Mark") stooges) ; -> nil
    ;在Lists搜索一个元素是线性低效的,在set里搜索就容易且高效多了
    (contains? (set stooges) "Moe") ; -> true
  • into
    把两个集合里面的元素合并成一个新的大list

    (into [1 2] [3 4]) ; -> [1 2 3 4]
    (into '(1 2 3) '(4 5 6)) ; -> (6 5 4 1 2 3)

Vectors

类似于数组。这种集合对于从最后面删除一个元素,或者获取最后面一个元素是非常高效的(O(1))。这意味着对于向vector里面添加元素使用conj被使用cons更高效。

  • 创建

    (def stooges (vector "Moe" "Larry" "Curly"))
    (def stooges ["Moe" "Larry" "Curly"])
  • get
    获取vector里面指定索引的元素,从map中取value也是用的get。get 和 nth 有点类似。

    ; Usage: (get map key) , (get map key not-found)
    (get stooges 1 "unknown") ; -> "Larry"
    (get stooges 3 "unknown") ; -> "unknown"
  • assoc
    可以对 vectors 和 maps进行操作。替换指定索引的元素。

    (assoc stooges 2 "Shemp") ; -> ["Moe" "Larry" "Shemp"]
  • subvec
    获取一个给定vector的子vector。

    ; Usage: (subvec v start) , (subvec v start end)
    (subvec stooges 1) ; -> ["Larry" "Curly"]
    (subvec stooges 1 2) ; -> ["Larry"]
    ```
    ### Sets
    Sets 是一个包含不重复元素的集合。Clojure 支持两种不同的set: 排序的(sorted-set)和不排序的(hash-set)。
    - 创建
    ```clojure
    (def stooges (hash-set "Moe" "Larry" "Curly")) ; not sorted
    (def stooges #{"Moe" "Larry" "Curly"}) ; same as previous
    (def stooges (sorted-set "Moe" "Larry" "Curly"))
  • contains?
    是否包含某元素。可以操作在set和map上。

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

Sets 自己也可以作为一个函数。当以这种方式来用的时候,返回值要么是这个元素,要么是nil。 这个比起contains?函数来说更简洁。

(stooges "Moe") ; -> "Moe"
(stooges "Mark") ; -> nil
(println (if (stooges person) "stooge" "regular person"))
  • disj
    去掉给定的set里面的一些元素,返回一个新的set。

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

Maps

Maps 保存从key到value的a对应关系 — key和value都可以是任意对象。和set类似,map也分为排序的(sorted-hash)和不排序的(hash-map)。

  • 创建

    (def colors (hash-map :red 1, :yellow 9, :blue 4, :black 3))
    (def colors {:red 1, :yellow 9, :blue 4, :black 3})
    ; -> {:red 1, :yellow 9, :blue 4, :black 3} 语法糖,和上面的是一样的
    (def colors_sort (sorted-map :red 1, :yellow 9, :blue 4, :black 3))
    ; -> {:black 3, :blue 4, :red 1, :yellow 9} 可以看到这是排序了的

    Map可以作为它的key的函数,同时如果key是keyword的话,那么key也可以作为map的函数。下面是三种获取:red所对应的值的方法。

    (get colors :red)
    (colors :green)
    (:red colors)
  • contains? & keys & vals
    contains? 用来检测某个key存不存在。keys 和 vals 可以获得map中的键集合和值集合。

    (contains? colos :red) ; -> true
    (keys colors) ; -> (:yellow :red :blue :black)
    (vals colors) ; -> (9 1 4 3)
  • assoc & dissoc
    assoc 会创建一个新的map,同时添加任意对新的key-value对, 如果某个给定的key已经存在了,那么它的值会被更新。

    dissoc 会创建一个新的map,同时去掉了给定的那些key。

    (assoc colors :red "red" :white "white")
    ; -> {:white "white", :yellow 9, :red "red", :blue 4, :black 3}
    (dissoc colors :red :white )
    ; -> {:yellow 9, :blue 4, :black 3}
  • 遍历
    遍历map可以使用宏doseq,把key bind到color, 把value bind到v。name函数返回一个keyword的字符串名字。

    (doseq [[color v] colors]
    (println (str "The value of " (name color) " is " v ".")))

输出为:

The value of yellow is 9.
The value of red is 1.
The value of blue is 4.
The value of black is 3.

或者使用Destructing

  • 内嵌
    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}}})

为了获取这个人的employer的address的city的值,我们一般有三种方法。

; 使用get-in函数
(get-in person [:employer :address :city])
; 宏 -> 本质上是调用一系列的函数,前一个函数的返回值作为后一个函数的参数.
(-> person :employer :address :city)
; reduce 函数接收一个需要两个参数的函数, 一个可选的value以及一个集合。
(reduce get person [:employer :address :city])

修改一个内嵌的key值,我们可以使用assoc-in或者update-in,不同之处在于update-in的新值是通过一个给定的函数来计算出来的。

(assoc-in person [:employer :address :city] "Clayton")
(update-in person [:employer :address :zip] str "-1234") ; :zip "63141-1234"

StructMaps

StructMaps 和普通map类似,它的作用其实是用来模拟java里面的javabean。

  • 定义

    (def vehicle-struct (create-struct :make :model :year :color))
    (defstruct vehicle-struct :make :model :year :color) ; 简洁的写法
  • 实例化

    使用struct函数实例化一个StructMap对象,相当于java里面的new关键字。提供给struct的参数的顺序必须和定义时提供的keyword的顺序一致,后面的参数可以忽略。如果忽略,那么对应key的值就是nil。

    (def vehicle (struct vehicle-struct "Toyota" "Prius" 2009))
  • accessor
    accessor 函数可以创建一个类似java里面的getXXX的方法, 它的好处是可以避免hash查找, 它比普通的hash查找要快。

    (def make (accessor vehicle-struct :make)) ; 注意使用的是def而不是defn
    (make vehicle) ; -> "Toyota"
    (vehicle :make) ; 效果一样只是慢些
    (:make vehicle) ; 效果一样只是慢些