Clojure 语法

Lisp方言有一个非常简洁的语法 — 有些人觉得很美的语法。数据和代码的表达形式是一样的,一个列表的列表很自然地在内存里面表达成一个tree。(a b c)表示一个对函数a的调用,而参数是b和c。如果要表示数据,你需要使用 '(a b c) o或者 (quote (a b c)) 。通常情况下就是这样了,除了一些特殊情况 — 到底有多少特殊情况取决于你所使用的方言。

我们把这些特殊情况称为语法糖。语法糖越多代码写起来越简介,但是同时我们也要学习更多的东西以读懂这些代码。这需要找到一个平衡点。很多语法糖都有对应的函数可以调用。到底语法糖是多了还是少了还是你们自己来判断吧。

下面这个表格简要地列举了Clojure里面的一些语法糖, 这些语法糖我们会在后面详细讲解的,所以如果你现在没办法完全理解它们不用担心。

作用 语法糖 对应函数
注释 ; _text_ 单行注释 宏(comment _text_)可以用来写多行注释
字符 (Java char 类型) \_char_ \tab \newline \space \u_unicode-hex-value_ (char _ascii-code_) (char \u_unicode_ )
字符串 (Java String 对象) "_text_" (str _char1_ _char2_ ...) 可以把各种东西串成一个字符串
关键字是一个内部字符串; 两个同样的关键字指向同一个对象; 通常被用来作为map的key :_name_ (keyword "_name_")
当前命名空间的关键字 ::_name_ N/A
正则表达式 #"_pattern_" (re-pattern _pattern_)
逗号被当成空白(通常用在集合里面用来提高代码可读性) , (逗号) N/A
链表(linked list) '(_items_) (不会evaluate每个元素) (list _items_) 会evaluate每个元素
vector(和数组类似) [_items_] (vector _items_)
set #{_items_} 建立一个hash-set (hash-set _items_) (sorted-set _items_)
map {_key-value-pairs_} 建立一个hash-map (hash-map _key-value-pairs_) (sorted-map _key-value-pairs_)
给symbol或者集合绑定元数据 #^{_key-value-pairs_} _object_ 在读入期处理 (with-meta _object_ _metadata-map_) 在运行期处理
获取symbol或者集合的元数据 ^_object_ (meta _object_)
获取一个函数的参数列表(个数不定的) & _name_ N/A
函数的不需要的参数的默认名字 _ (下划线) N/A
创建一个java对象(注意class-name后面的点) (_class-name_. _args_) (new _class-name_ _args_)
调用java方法 (. _class-or-instance_ _method-name_ _args_) 或者 (._method-name_ _class-or-instance_ _args_) N/A
串起来调用多个函数,前面一个函数的返回值会作为后面一个函数的第一个参数;你还可以在括号里面指定额外参数;注意前面的两个点 (.. _class-or-object_ (_method1 args_) (_method2 args_) ...) N/A
创建一个匿名函数 #(_single-expression_)% (等同于 %1 ), %1 , %2来表示参数 (fn [_arg-names_] _expressions_)
获取Ref, Atom 和Agent对应的valuea @_ref_ (deref _ref_)
get Var object instead of the value of a symbol (var-quote) #'_name_ (var _name_)
syntax quote (使用在宏里面) ``` none
unquote (使用在宏里面) ~_value_ (unquote _value_)
unquote splicing (使用在宏里面) ~@_value_ none
auto-gensym (在宏里面用来产生唯一的symbol名字) _prefix_# (gensym _prefix_ )

对于二元操作符比如 +和*, Lisp方言使用前置表达式而不是中止表达式,这和一般的语言是不一样的。比如在java里面你可能会写 a + b + c , 而在Lisp里面它相当于 (+ a b c) 。这种表达方式的一个好处是如果操作数有多个,那么操作符只用写一次 . 其它语言里面的二元操作符在lisp里面是函数,所以可以有多个操作数。

Lisp代码比其它语言的代码有更多的小括号的一个原因是Lisp里面不使用其它语言使用的大括号,比如在java里面,方法代码是被包含在大括号里面的,而在lisp代码里面是包含在小括号里面的。

比较下面两段简单的Java和Clojure代码,它们实现相同的功能。它们的输出都是: “edray” 和 “orangeay”.

// This is Java code.
public class PigLatin {

  public static String pigLatin(String word) {
  char firstLetter = word.charAt(0);
  if ("aeiou".indexOf(firstLetter) != -1) return word + "ay";
  return word.substring(1) + firstLetter + "ay";
  }

  public static void main(String args[]) {
  System.out.println(pigLatin("red"));
  System.out.println(pigLatin("orange"));
  }
}
; This is Clojure code.
; When a set is used as a function, it returns a boolean
; that indicates whether the argument is in the set.
(def vowel? (set "aeiou"))

(defn pig-latin [word] ; defines a function
  ; word is expected to be a string
  ; which can be treated like a sequence of characters.
  (let [first-letter (first word)] ; assigns a local binding
    (if (vowel? first-letter)
      (str word "ay") ; then part of if
      (str (subs word 1) first-letter "ay")))) ; else part of if

(println (pig-latin "red"))
(println (pig-latin "orange"))

Clojure支持所有的常见数据类型比如 booleans ( true and false ), 数字, 高精度浮点数, 字符(上面表格里面提到过 ) 以及字符串. 同时还支持分数 — 不是浮点数,因此在计算的过程中不会损失精度.

Symbols是用来给东西命名的. 这些名字是被限制在名字空间里面的,要么是指定的名字空间,要么是当前的名字空间. Symbols的值是它所代表的名字的值. 要使用Symbol的值,你必须把它用引号引起来.

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

和任何语言一样,你可以写出很难懂的Clojure代码。遵循一些最佳实践可以避免这个。写一些简短的,专注自己功能的函数可以使函数变得容易读,测试以及重复利用。经常使用“抽取方法”的模式来对你的代码进行重构。高度内嵌的函数是非常难懂得,千万不要这么写, 你可以使用let来帮助你。把匿名函数传递给命名函数是非常常见的,但是不要把一个匿名函数传递给另外一个匿名函数, 这样代码就很难懂了。