Programming Clojure笔记之二——探索Clojure

Forms

Clojure具有同像性(homoiconic),即代码与其数据的结构一致。Clojure代码主要由以下形式(Forms)构成。这些形式可以理解为其他程序设计语言中的数据类型。

Form Example(s)
Boolean true, false
Character \a
Keyword :tag, :doc
List (1 2 3), (println “hello”)
Map {:name “Bill”, :age 42}
Nil nil
Number 1, 4.2
Set #{:snap :crackle :pop}
String “hello”
Symbol user/foo, java.lang.String
Vector [1 2 3]

数值(Number)

数值的字面量是一种Form,对其求值得到本身。

42 -> 42

;vector is a form
[1 2 3] -> [1 2 3]

;list is a form,此处解析为函数调用
(+ 1 2) -> 3
(+) -> 0
(concat [1 2] [3 4]) -> [1 2 3 4]
(> 5 2) -> true

;整数相除得到一个Clojure类型Ratio
(/ 22 7) -> 22/7
;如果需要实际的除法运算,使用浮点数字面量
(/ 22.0 7) -> 3.142857142857143
;或者这样得到整数相除的商和余数
(quot 22 7) -> 3
(rem 22 7) -> 1
;任意精度的浮点数运算在数字后加上M,结果是一个BigDecimal
(+ 1 (/ 0.00001 1000000000000000000)) -> 1.0
(+ 1 (/ 0.00001M 1000000000000000000)) -> 1.00000000000000000000001M
;任意精度的整数运算,则在其中一个数字后加上N即可,结果是一个BigInt
(* 1000N 1000 1000 1000 1000 1000 1000) -> 1000000000000000000000N

符号(Symbol)

符号用来给Clojure中的事物命名,如:
- 类似str和concat的函数。
- 类型+和-的操作符,本质上也是函数。
- Java类,如java.lang.String
- 名字空间如clojure.core和Java包如java.lang
- 数据结构和引用

String和Character

String也是一种Form。Clojure中的String就是Java中的String。

(.toUpperCase "hello") -> "HELLO"

函数前的点表明这是调用的Java方法而不是Clojure函数。Clojure没有封装所有的函数,但是toString得到了封装。这就是str函数。(str & args)
该函数将多个参数并在一起,并且可以略过nil。

(str 1 2 nil 3) -> "123"

Clojure中的字符也是Java字符。字面量表达方式为\{letter}

(str \h \e \y \space \y \o \u)
-> "hey you"

Clojure也没有封装Java的字符函数。只能直接使用Java方法。

(Character/toUpperCase \s) -> \S

字符串就是字符序列。在字符串上调用序列函数就得到了一个字符序列。例如交插两个字符串:

(interleave "Attack at midnight" "The purple elephant chortled")
-> (\A \T \t \h \t \e \a \space \c \p \k \u \space \r \a \p \t \l \space \e \m \space \i \e \d \l \n \e \i \p \g \h \h \a \t \n)

;如果要将结果转为字符串,不能简单的使用str
(str (interleave "Attack at midnight" "The purple elephant chortled"))
-> "clojure.lang.LazySeq@d4ea9f36"

;应该使用apply函数
(apply str (interleave "Attack at midnight" "The purple elephant chortled"))
-> "ATthtea cpku raptl em iedlneipghhatn"

;最后,将交插的结果还原
(apply str (take-nth 2 "ATthtea cpku raptl em iedlneipghhatn"))
-> "Attack at midnight"

Booleans和nil

  • true 的值为true,false的值为false
  • nil在布尔上下文中被解析为false
  • 除了false和nil,其他所有值在布尔上下文中解析为true
    谓词函数(一般以?结尾的函数)用于判定表达式的真实值。
(true? true) -> true
(true? "foo") -> false
(zero? 0.0) -> true
(zero? (/ 22 7)) -> false

Maps、Keywords和Records

;定义一个Map
(def inventors {"Lisp" "McCarthy" "Clojure" "Hickey"}) -> #'user/inventors
;或者在键值对之间加上逗号(Clojure将之视为空格)
(def inventors {"Lisp" "McCarthy", "Clojure" "Hickey"}) -> #'user/inventors

;Map是函数,传入键,得到相应的值
(inventors "Lisp") -> "McCarthy"
(inventors "Foo") -> nil
;或者使用get函数,可以传入一个缺省值
(get inventors "Lisp" "I dunno!") -> "McCarthy"
(get inventors "Foo" "I dunno!") -> "I dunno!"

;Keyword类似于symbol,不过以冒号开头。并且总是解析为自身,symbol则指向其他值。使用Keyword作为Map的键
(def inventors {:Lisp "McCarthy" :Clojure "Hickey"}) -> #'user/inventors
;使用keyword获取map中关联的值
(inventors :Clojure) -> "Hickey"
;keyword也是函数,因而这样也可以获取map中的值。
(:Clojure inventors) -> "Hickey"

;如果很多map有相同的键,这时可以定义一个record
(defrecord Book [title author]) -> user.Book
;实例化一个记录并且绑定到变量b
(def b (->Book "Anathem" "Neal Stephenson"))
;另一种实例化record的方法
(Book. "Anathem" "Neal Stephenson") -> #user.Book{:title "Anathem", :author "Neal Stephenson"} 
;从变量b中获取值
(:title b) -> "Anathem"

Reader macros

Clojure中的forms是由reader读取并转化为数据结构的。reader被一些特殊的字符触发后的特殊行为称之为reader macro。最典型的例子就是注释,这时macro字符就是分号,触发的特殊行为就是,忽略直到行尾的所有内容。
reader macros一般是冗长list forms的省略形式。阻止求值的单引号就是其中之一。

'(1 2) -> (1 2) ;'(1 2) is equivalent to the longer (quote (1 2)):
(quote (1 2)) -> (1 2)

下表列出了其他的reader macro。

Reader Macro Example(s)
匿名函数(Anonymous function) #(.toUpperCase %)
注释(Comment) ;sigle-line comment
解引用(Deref) @form=>(deref form)
元数据(Metadata) ^metadata form
引用(Quote) ‘form=>(quote form)
正则表达式模式(Regex pattern) #"foo"=>a
Syntax-quote `x
Unquote ~
Unquote-splicing ~@
Var-quote #'x=>(var x)

函数

使用defn定义函数如下:

;基本形式
(defn name doc-string? attr-map? [params*] body)

(defn greeting
"Returns a greeting of the form 'Hello, username.'"
[username]
(str "Hello, " username))
;调用函数
(greeting "world")
-> "Hello, world"
;查看函数的文档
(doc greeting)

;不提供参数调用greeting会产生异常。事实上,函数可以有多个参数列表和相应的函数体
;以下是函数定义的完整形式
(defn name doc-string? attr-map? ([params*] body)+)
(defn greeting
"Returns a greeting of the form 'Hello, username.' Default username is 'world'."
([] (greeting "world"))
([username] (str "Hello, " username)))

;函数的不定参数
(defn date [person-1 person-2 & chaperones]
(println person-1 "and" person-2 "went out with" (count chaperones) "chaperones."))

匿名函数

;不使用匿名函数
(defn indexable-word? 
    [word] (> (count word) 2))

(require '[clojure.string :as str])
(filter indexable-word? (str/split "A fine day it is" #"\W+"))
-> ("fine" "day")

;匿名函数定义方式
(fn [params*] body)
;使用匿名函数
(filter (fn [w] (> (count w) 2)) (str/split "A fine day" #"\W+"))
-> ("fine" "day")
;使用隐式参数(%1,%2...)使得匿名函数更加简洁
;#(body)
(filter #(> (count %) 2) (str/split "A fine day it is" #"\W+"))
-> ("fine" "day")
;在函数内部将匿名函数绑定到symbol
(defn indexable-words [text]
    (let [indexable-word? (fn [w] (> (count w) 2))]
        (filter indexable-word? (str/split text #"\W+"))))

;匿名函数作为返回值
(defn make-greeter [greeting-prefix]
    (fn [username] (str greeting-prefix ", " username)))

变量(var)、绑定(Binding)和命名空间(Namespace)

当使用def或者defn定义了一个对象时,该对象就存储在一个var中。

(def foo 10) -> #'user/foo
;符号user/foo指向一个变量,变量绑定了值10。你可以直接引用一个var(var foo) -> #'user/foo
;引用一个var,更常见的是使用其reader macro形式(#')
#'foo -> #'user/foo
;一般情况下不需要直接引用var,可以忽略symbol和var的区别。
;需要注意的是,var不仅仅只能存储值。还可以包含元数据等等。

绑定

var绑定到了名字,其他绑定还包括实参值绑定到了函数形参名。函数的参数绑定具有词法作用域(lexical scope),仅仅在函数体内可见。然而函数不是进行词法绑定的唯一方式。let这种特殊的form是专门用来进行词法绑定的。

(let [bindings*] exprs*)
;绑定在exprs有效,exprs中最后一个表达式的值就是let的值。
(defn square-corners [bottom left size] (let [top (+ bottom size) right (+ left size)] [[bottom left] [top left] [top right] [bottom right]]))

解构(Destructuring)

有时函数只需要容器数据结构的一部分数据,而不想将参数绑定到整个容器。Clojure中使用destructuring来解决这个问题

;使用解构前
(defn greet-author-1 [author]
    (println "Hello," (:first-name author)))
(greet-author-1 {:last-name "Vinge" :first-name "Vernor"})
| Hello, Vernor

;使用解构后
(defn greet-author-2 [{fname :first-name}]
    (println "Hello," fname))
(greet-author-2 {:last-name "Vinge" :first-name "Vernor"})
| Hello, Vernor

;前面使用map来解构关联容器,那么vector可以用来解构顺序容器。
(let [[x y] [1 2 3]]
[x y])
-> [1 2]
;依照惯例使用下划线绑定不关心的值
(let [[_ _ z] [1 2 3]]
z)
-> 3

;使用:as来同时绑定全部数据
(let [[x y :as coords] [1 2 3 4 5 6]]
  (str "x: " x ", y: " y ", total dimensions " (count coords)))
-> "x: 1, y: 2, total dimensions 6"

;综合示例
(require '[clojure.string :as str])
(defn ellipsize [words]
  (let [[w1 w2 w3] (str/split words #"\s+")]
    (str/join " " [w1 w2 w3 "..."])))
(ellipsize "The quick brown fox jumps over the lazy dog.")
-> "The quick brown ..."

命名空间

;确认当前命名空间存在一个符号,不存在则返回nil
(resolve 'foo)
-> #'user/foo
;切换命名空间,不存在则创建
user=> (in-ns 'myapp) 
-> #<Namespace myapp> 
myapp=>

;切换到新的命名空间后java.lang包会自动导入,然而需要手动导入clojure.core
myapp=> (clojure.core/use 'clojure.core)
-> nil
;导入其他Java包
(import '(java.io InputStream File))
-> java.io.File
(.exists (File. "/tmp"))
-> true
;声明当前文件的命名空间并导入clojure和Java包
(ns examples.exploring
  (:require [clojure.string :as str])
  (:import (java.io File)))

调用Java

;创建对象
(def rnd (new java.util.Random)) -> #'user/rnd
;调用方法
(. rnd nextInt 10) -> 8
;静态方法
(. Math PI) -> 3.141592653589793
;javadoc函数
(javadoc java.net.URL)

控制流

if分支

(defn is-small? [number] (if (< number 100) "yes" "no"))

使用do引入副作用

;do接受任意数量的form并且依次求值,返回最后的form值
(defn is-small? [number] (if (< number 100) "yes" (do (println "Saw a big number" number) "no")))

使用loop/recur进行迭代

(loop [result [] x 5] 
  (if (zero? x)
    result
    (recur (conj result x) (dec x)))) -> [5 4 3 2 1]

;一般不用loop/recur,使用下列方法作为替代
(into [] (take 5 (iterate dec 5))) -> [5 4 3 2 1]
(into [] (drop-last (reverse (range 6)))) -> [5 4 3 2 1]
(vec (reverse (rest (range 6)))) -> [5 4 3 2 1]

for

;Clojure中的for用来sequence comprehension
(defn index-filter [pred coll] 
  (when pred
    (for [[idx elt] (indexed coll) :when (pred elt)] idx)))

(index-filter #{\a \b} "abcdbbb") -> (0 1 4 5 6)
(index-filter #{\a \b} "xyz") -> ()

元数据

;str变量的元数据
(meta #'str)
-> {:ns #<Namespace clojure.core>,
    :name str,
    :file "core.clj",
    :line 313,
    :arglists ([] [x] [x & ys]),
    :tag java.lang.String,
    :doc "With no args, ... etc."}

;使用元数据reader macro添加元数据,元数据由编译器添加到var中
^metadata form

;定义了shout函数,并添加了:tag元数据。表明函数接受String类型返回String类型。
(defn ^{:tag String} shout [^{:tag String} s] (.toUpperCase s))
-> #'user/shout

;由于:tag键很常用,因此简写形式如下
(defn ^String shout [^String s] (.toUpperCase s))
-> #'user/shout

;如果觉得元数据影响函数阅读,可以放到最后
(defn shout
  ([s] (.toUpperCase s))
  {:tag String})

你可能感兴趣的:(函数式编程,clojure)