Clojure学习笔记(二):语法

定义函数

  • 匿名函数
    匿名函数(fn) (fn [x y] (+ x y)) 创建一个匿名函数, fn 和 lambda 类似,fn还有一个简写形式 #(+ %1 %2)。如果只有一个参数,那么可以用 % 代替 %1

  • def
    def可以将一个匿名函数绑定到一个name上。

    (def my-add (fn [x y] (+ x y)))
  • defn

    defn 是 def 与 fn 的简写。一般我们也都是用defn。

    (defn my-add
    "this is a comment"
    [x y]
    (+ x y))
    (println (my-add 3 4)) ; -> 7

    ; 函数的重载
    (defn my-add
    ([x y]
    (+ x y))
    ([x y z]
    (+ x y z)))
    (println (my-add 3 4)) ; -> 7
    (println (my-add 3 4 5)) ; -> 12
  • declare
    函数定义必须在函数调用前,如果想在定义前使用函数,必须声明它(declare function-names)

  • defn- & defmacro-
    defn- 定义私有函数,他们仅在自己的 namespace 可见

  • 参数的解构
    解构可以用在一个函数或者宏的参数里面来把一个集合里面的一个或者几个元素抽取到一些本地binding里面去。它可以用let,&,:as等。

    (defn my-add [numbers]
    (let [n1 (first numbers)
    n3 (nth numbers 2)]
    (+ n1 n3)))
    (my-add [1 2 3 4]) ; -> 4

&符号可以在解构里面用来获取集合里面剩下的元素。:as关键字可以用来获取对于整个被解构的集合的访问。

调用Java

  • 导入

    (import
    '(java.util Calendar GregorianCalendar)
    '(javax.swing JFrame JLabel))
  • 创建对象

    使用new关键字创建对象(new class-name args),也可以使用语法糖简化(class-name. args)

    例如:(new String "abc") 或者 (String. "abc")

  • 方法调用

    使用.(. class-or-instance method-name args) 或者 (.method-name class-or-instance args)

    例如:

    (def s (new String "abc"))
    (. s charAt 0) ; -> \a
  • 连续调用方法
    (.. class-or-object (method1 args) (method2 args) ...) 前一个函数的返回值,作为后一个函数的第一个参数。

    例如:

    (.. s (toString) (charAt 1)) ; -> \b
  • set!
    set! : (set! (. target name) value)
  • 静态变量/静态方法

    对于静态方法和静态变量,可以使用ClassName/field的方式来调用。例如:

    Integer/MIN_VALUE ; -> -2147483648
    (Integer/parseInt "101") ; -> 101

条件控制

  • if
    if的语法是(if condition then-expr else-expr),第一个参数是条件,第二个表达式是条件成立时执行,第三个表达式可选,在条件不成立时执行。如果需要执行多个表达式,包在do里。

    (if is-weekend
    (println "play")
    (do (println "work")
    (println "sleep"))))
  • when & whennot
    宏when 和when-not 提供和if类似的功能,用它们可以更方便地写inline condition。

    (when is-weekend (println "play"))
    (when-not is-weekend (println "work") (println "sleep"))
  • if-let & when-let
    if-let给一个变量绑定一个值,如果这个值为 true,则选择一个语句来执行,否则选择另一个。
    when-letif-let类似,不同之处还是在于没有else分支。

    (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
  • condp
    condp 宏跟其他语言里面的switch/case语句差不多。它接受两个参数,一个谓词参数 (通常是= 或者instance?) 以及一个表达式作为第二个参数。

    (defn choose [value]
    (condp = value
    1 "one"
    2 "two"
    3 "three"
    (str "unexpected value, " value )))

    (choose 2) ; -> "two"
    (choose 8) ; -> "unexpected value, 8"
  • cond
    cond 宏接受任意个 谓词/结果表达式 的组合。按照顺序测试所有谓词,直到有一个为true,返回其结果。有点像一直else if

    (defn chosse [t]
    (println
    (cond
    (instance? String t) "invalid temperature"
    (<= t 0) "freezing"
    (>= t 100) "boiling"
    true "neither")))

迭代

  • dotimes
    dotimes执行给定的表达式一定次数, 一个本地binding会被给定值:从0到一个给定的数值。如果这个本地binding是不需要的,也可以用_来代替。

    (dotimes [card-number 3]
    (println "deal card number" card-number))

输出为

deal card number 0
deal card number 1
deal card number 2
  • while
    while 会一直执行一个表达式只要指定的条件为true。

    (def c 3)
    (while (> c 0)
    (print ".")
    (def c (dec c)))

    ; -> ...
  • dorun+for & doseq
    doseq用来遍历集合在上面已经讲过了。

    for的执行主体只可以采用单行语句,而doseq可以采用多行语句,并且for产生 lazy sequence。同时用:when:while还可以做一些过滤,它们的区别在于:when会迭代所有bindings并执行满足条件的主体,:while会迭代所有bindings并执行满足条件的主体 直到 条件为false后跳出。

(def cols "ABCD")
(def rows (range 1 4)) ; purposely larger than needed to demonstrate :while

(println "for demo")
(dorun
(for [col cols :when (not= col \B)
row rows :while (< row 3)]
(println (str col row)))) ; -> 只可单行

(println "\ndoseq demo")
(doseq [col cols :when (not= col \B)
row rows :while (< row 3)]
(print col) ; 可以
(println row)) ; 多行

执行结果如下,两段代码的结果是一致的。

for demo
A1
A2
C1
C2
D1
D2

doseq demo
A1
A2
C1
C2
D1
D2

递归

由于jvm的关系,clojure不会自动进行尾递归优化(tail call optimization),在尾递归的地方,你应该明确的使用 recur 这个关键词,而不是函数名。

(defn my-add [x y]
(if (zero? x)
y
(my-add (dec x) (inc y))))

(defn my-add [x y]
(if (zero? x)
y
(recur (dec x) (inc y))))

第一个不会进行尾递归优化,第二个会进行尾递归优化。

looprecur配合可以实现和循环类似的效果。

(loop [n number factorial 1]
(if (zero? n)
factorial
(recur (dec n) (* factorial n)))))

注意,recur只能出现在special form的最后一行。

谓词

false 和 nil 解释为 false
true 和其他任意值,包括 0 都被认为是 true

  • 测试两值关系
    <,<=,=,not=,==,>,>=,compare,distinct? 以及identical?.

  • 测试逻辑关系
    and,or,not,true?,false? 和nil?

  • 测试集合
    empty?,not-empty,every?,not-every?,some? 以及not-any?.

  • 测试数字
    even?,neg?,odd?,pos? 以及zero?.

  • 测试对象类型
    class?,coll?,decimal?,delay?,float?,fn?,instance?,integer?,isa?,keyword?,list?,macro?,map?,number?,seq?,set?,string? 以及vector?

命名空间(namespace)

  • require
    导入clojure库。

    (require 'clojure.string) ; 注意前面的单引号

clojure里面命名空间和方法名之间的分隔符是/而不是java里面使用的

(clojure.string/join "$" [1 2 3]) ; -> "1$2$3"
  • alias
    alias 函数给一个命名空间指定一个别名来简化操作。以及处理类名冲突问题。

    (alias 'su 'clojure.string)
    (su/join "$" [1 2 3]) ; -> "1$2$3"
  • refer
    refer可以使指定的命名空间里的函数在当前命名空间里可以不需要包名前缀就能访问。

    (refer 'clojure.string)
    ; 上面的代码可以写成
    (join "$" [1 2 3]) ; -> "1$2$3"
  • use
    我们通常把require 和refer 结合使用, 所以clojure提供了一个use , 它相当于require和refer的简洁形式。

    (use 'clojure.string)
  • ns
    ns宏可以改变当前的默认名字空间。它支持这些指令::require,:use:import,后面的参数不需要quote。这些其实是它们对应的函数的另外一种方式,一般建议使用这些指令而不是函数。

    (ns com.example.library
    (:require [clojure.contrib.sql :as sql])
    (:use (com.example one two))
    (:import (java.util Date Calendar)
    (java.io File FileInputStream)))

宏(Macro)

宏是在读入期(而不是编译期)就进行实际代码替换的一个机制。

  • 反引号(`)
    防止宏体内的任何一个表达式被evaluate。这意味着宏体里面的代码会原封不动地替换到使用这个宏的所有的地方 – 除了以波浪号开始的那些表达式。

  • ~~@
    当一个名字前面被加了一个波浪号,并且还在反引号里面,它的值会被替换的。如果这个名字代表的是一个序列,那么我们可以用 ~@ 来替换序列里面的某个具体元素。

  • id#
    在一个标识符背后加上 # 意味着生成一个唯一的symbol, 比如 foo# 实际可 能就是 foo_004 可以看作是let与gensym的等价物,这在避免符号捕捉时很有用。