为了全面学习函数式编程语言,之前尝试了scala,不得不承认它的入门门槛略高。于是乎,决定从古老的新语言Clorue入手,作为Lisp的一种方言,语法上无疑是古老的,但它又运行在JVM上且有一些不错的机制,无疑是门新语言。


  • 满篇的括号

Lisp语法最大特点就是一堆圆括号,任何函数调用都以“(”开始、“)”结束,函数名或函数本身与参数在括号里呈现前缀表达法。

1
2
3
4
5
(+ 1 3) ;=4
(add 3 5) ;=8
(or (= 1 2) (> 4 6)) ;=false
;下面为匿名函数
(#(apply * %) '(1 3 4 5 6)) ;=360

除了圆括号外,也有方括号“[]”,在clojure中,它既可以是向量的字面量,也可是函数的参数列表。

1
2
3
4
5
(def a_vector [1 3 4 5 6 6]) ;=#'user/a_vector
;或者是个函数的参数列表
(defn a_function [param1 param2]
(do
(print "i am function")))

当然少不了花括号“{}”,在Map与Set两种数据结构的字面量中体现。

1
2
{:key1 val1 :key2 val2} ;这是Map
#{ 1 2 3 } ;这是个Set

扯句无关的,在最原始的Lisp中几乎只有圆括号,例如与之最相近的一种方言Common Lisp。

  • 优先级取决于括号

与其他语言一样,括号总伴随着表达式求值的优先级。

1
(+ (* (- 4 2) (/ 10 5)) (- 1 (+ 4 5)) 2) ;=5

在Lisp语言中,我们通常很容易获知代码的优先级,正如代码本身就是一棵抽象语法树,能快速知道最先被求值的是哪个节点。代码自身等同于AST,我们称之为“同像性”。

正因Clojure的“同像性”,也为它自己带来其他语言没有的特性—可以自定义语法糖,即自定义宏。

  • 括号外的一些符号—字面量

要知道,Clojure是一种语法极其简易的语言,括号外也有一些能使代码简明易懂的符号,它们被叫做“字面量”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
;字符串字面量
"i am string"
;数字字面量
54321 ;十进制,clojure只有long和double类型
10.1111333 ;double
16rffa ;十六进制
2r101 ;二进制
;其他基本类型
true
false
nil
\c ;字符
;""前面加个#就是正则表达式
(re-seq #"\d*-\w+" "omg123-string") ;=("123-string")
;关键字
;在Map数据结构中,关键字字面量是很有用的数据类型
:keyname ;=:keyname 代表自己
;数据结构字面量
;map
{:key1 val1 :key2 val2}
;list
'(1 3 4 5 :keyname "item")
;vector
[1 3 4 5 :keyname "item"]
;set
#{1 2 3}
;与表达式、函数相关的字面量
;clojure有个叫quote的东西,它被用来阻止表达式求值,其字面量为'
;可理解为只是个简单声明一个list,这与上面说到的同像性有关
'(+ 1 3) ;=(+ 1 3) ;变成了一个list,其中+等同于另外两个元素,被作为一个值塞进list中
;函数的字面量 #
#(+ %1 %2) ;代表了一个匿名函数
(def my_add #(+ %1 %2)) ;将匿名函数赋值给my_add
(my_add 1 3) ;=4
  • 代码块以及值的作用域

每个函数调用都有圆括号包裹起开,执行多条语句同样需要一个圆括号。一个括号里的多条语句,叫做代码块。
clojure用宏do来依次执行代码块中的语句,且以最后的语句的结果作为返回值。

1
2
3
4
(do
(println "1 + 2")
(+ 2 3)
(+ 1 2))

在fn、defn、let、loop、try等语义中,已经隐式使用了do,所以并不需要显示声明调用do。

1
2
3
4
(defn no_do []
((println "1 + 2")
(+ 2 4)
(+ 1 2)))

与代码块息息相关的还有值的作用域,上面用def定义的值,都可在其命名空间全局访问。

1
2
(ns i_am_namespace)
(def x_in_i_am_namespace) ;=#'i_am_namespace/x_in_i_am_namespace

有时我们需要局部域的值,clojure为我们提供let,叫做本地绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
(def z "def_z")
(let [x (list 1 2 3 4)
y (vector 1 3 4 4)]
(do
;do something
(println z)
(conj x z)))
(println z);=def_z
;报错,x只在定义它的let内有效
(println x);=CompilerException java.lang.RuntimeException: Unable to resolve symbol: x in this context

  • 入参也可以很方便—let参数的解构

clojure有两类重要的数据结构list和map,参数很多时候也需要接受这种数据结构。

有时我们需要若干参数整合或求值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;如果只是打印出1+2等式出来,也许我们会这样写
(let [x 1
y 2
r "1 + 2 ="]
(println r (+ x 1)))
;显然一个个去赋值,看起来很蠢,也不方便,clojure允许这样写。
(let [[x y r] '(1 2 "1 + 2 =")]
(println r (+ x 1)))
;倘若我们只需要计算一个长列表中的几项元素,可以这样写
(let [[x y z r] '(1 2 3 "i am string" 4 5)]
(println r)
(println (+ x y z)))
;或者
(let [[a b c _ d e] '(1 2 3 "i am string" 4 5)]
(println (+ a b c d e)))

这种对列表或向量解构,叫做顺序解构。

还有一种,叫做map解构。顾名思义,是对map进行解构。

让我们先定义个比较复杂的map,一名员工的家庭地址和单位地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(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}}})
;根据一个已知的map结构,我们打印这个叫Mark的哥们在哪里工作。
(let [{name :name
{emp_name :name} :employer
{{emp_street :street} :address} :employer} person]
(println name "work in" emp_name))
;这种结构方式有个相当不错的用处,当你确定一个json模型时,可以方便地对其进行处理计算。
;当然,也有难以预料的时候,也许模型中某个值缺失了,let的结构又能帮你设置默认值。
;:or可以帮你为不确定的健值设置默认值,它负责判断该健是否存在、值是否为nil
(let [{name :name
phone :phone
{emp_name :name} :employer
{{emp_street :street} :address} :employer
:or {phone "13700000000"}} person]
(println name "work in" emp_name "\nThe phone:" phone))

这里只说到let的结构,还有函数参数的结构,于此大同小异,等专门说到函数定义再聊。