程序结构
函数定义
defn
宏展开为def
和fn
调用的组合。fn
宏接受方括号中的一系列参数然后是程序主体,fn
形式可以用于定义匿名函数。
1 | (defn addition-function [x y] |
let 形式
let
形式接受一个向量作为其第一个参数,该向量包含偶数个形式,然后是在 let 求值时进行求值的 0 个或者多个形式。let 形式可以在代码中将一个符号和某个值绑定,从而引入局部命名对象。let 返回的是最后一个表达式的值。
1 | (let [x 1 |
以下函数可以使用 let 将其分成几个部分,使代码清晰。
1 | (defn average-pets [] |
在 let 中如果不需要关注值,可以使用下划线标识符,下划线标识符本身没有什么特别之处,只是 clojure 的一个惯例。在解构中,下划线标识符更加实用:(let [[_ _ z] [1 2 3]] z)
。
1 | (defn average-pets [] |
do
纯函数语言中,程序是没有副作用的,函数的唯一行为就是计算一个值并返回。但是现实世界中必然充满了状态,也必然有副作用,例如向控制台或者日志文件中打印某些内容、在数据库中保存内容就是改变世界状态的副作用。为了将多个表达式转为一个形式,clojure 提供了 do
形式。
1 | (if (is-something-true?) |
1 | (for [i (range 1 3)] (do (println i) i)) |
程序流程
条件
if
(if test consequent alternative)
if
形式接受一个测试表达式,若为真则求后续表达式(consequent),若为假则则使用替代形式(alternative)。形式可以结合do
形式使其完成多项工作。
1 | (if (> 5 2) "yes" "no") |
if-not
1 | (if-not (> 5 2) "yes" "no") |
cond
cond
可以将嵌套的if
条件数扁平化,(cond & clauses)
。
1 | (def x 1) |
子句(clauses)是成对的表达式。当一个表达式返回 true,求值相关的后续表达式并返回。 如果所有表达式都没有返回真值,则可以传入取真值的表达式(比如关键词 :default),然后求值相关的后续表达式并返回。
when
when
宏是将一个if
和一个隐式的do
。
1 | (when (> 5 2) |
此处就没有必要将do
包装这三个函数了,when
宏会负责这项工作。
when-not
1 | (when-not (< 5 2) |
逻辑函数
and
接受 0 个或多个形式,按顺序求值每个形式,如果任何一个返回 nil 或者 false,则返回该值。如果所有形式都不返回 false 或 nil,则 and 返回最后一个形式的值。如果没有任何值,则返回 true。
1 | (and) |
or
接受 0 个或多个形式并逐一求值,如果任何形式返回逻辑真值,则将该值返回。如果所有形式都不返回逻辑真值,则返回最后一个值。
1 | (or) |
not
该函数始终返回true
或者false
。
1 | (not true) |
比较函数
<
、<=
、>
、>=
、=
有一个额外特性:可以取任意数量的参数。
1 | ; < 可检测是否以升序排列 |
= 与 ==
=
函数等同于 Java 的 equals
,但适用于范围更广的对象,包括 nil、数值、序列。Clojure 中的 ==
函数只能用于比较数值。=
可以比较任意两个值,但比较三种不同类型的数值时结果不理想。
1 | (= 1 1N 1/1) |
如果对比不同类型的数值,则可以用==
代替,但是所有参数必须是数值。
1 | (== 1 1N 1/1) |
如果你预计所有要对比的数据都是数值,且预期有不同类型的数字,则使用==
,否则使用=
。
函数式循环
大部分函数式语言都不支持传统的for
循环结构,因为for
的典型实现需要改变循环计数器的值。作为替代,它们使用递归和函数应用。
while
while
宏与命令式语言类似。
1 | (while (request-on-queue?) |
loop/recur
Clojure 没有传统的for
循环,其循环流程控制是使用loop
和recur
。
1 | (defn fact-loop [n] |
loop
建立和let
形式完全相同的绑定,recur
也有两个绑定值(def current)
和(* fact current)
,他们在计算之后重新与current
和fact
绑定。
recur
看起来像递归,实际上不适用栈。recur
仅能作用于代码尾部,如果企图从任何其他位置使用它,编译器会报错。
doseq 和 dotimes
1 | (defn run-report [user] |
上面这个例子中doseq
的第一个项是一个新符号,以后将绑定到第二个项(必须是一个序列)中的每一个元素。形式的主体将对序列中的每个元素执行,然后整个形式将返回 nil。
dotimes
与之类似,接受一个向量(包含一个符号和一个数值),向量中符号被设置为 0 到 (n-1) 的值,并对每个数值求取主体的值。
1 | (dotimes [x 5] |
将打印数字 0~4,返回 nil。
map 、filter、remove、reduce、for
在前篇文章中的数据结构 — 序列中的“序列转换”一节中已提及。这里做一些补充。
- map
1 | (map inc [0 1 2 3]) |
- filter
1 | (defn non-zero-expenses [expenses] |
remove
filter
判定保留哪些元素,remove
判定抛弃哪些元素。两者刚好相反。
1 | (defn non-zero-expenses [expenses] |
- reduce & reductions
reduce
接受一个函数(有两个参数)和一个数据元素序列。函数参数应用到序列的前两个元素,产生第一个结果,之后使用这个结果和序列的下一个元素再次调用同一个函数。重复此过程直到处理完最后一个元素。
1 | (defn factorial [n] |
reduce
只返回最终的规约值,而reductions
返回每个中间值组成的序列。
1 | (defn factorial-steps [n] |
- for
可以使用的限定词::let
,:when
,:while
1 | (for [x [0 1 2 3 4 5] |
1 | (defn prime? [x] |
串行宏
thread-first
1 | (defn final-amount [principle rate time-periods] |
以上函数定义不易理解,需要从里往外读。使用thread-first
宏->
改写如下。该宏所做的是取第一个参数,将其放在下一个表达式的第二个位置。之所以成为thread-first
,是因为它将代码移到下一个形式首个参数的位置。之后,它取得整个结果表达式,并将其移到再下一个表达式的第二个位置。
1 | (defn final-amount-> [principle rate time-periods] |
thread-last
thread-last
宏->>
在取得第一个表达式结果后,将其移入下一个表达式最后的位置。之后,对所有表达式重复该 过程。
1 | (defn factorial [n] |
thread-last
宏更常见的用途是处理数据元素序列以及使用 map、reduce、filter 这样的高阶函数。这些函数都接受序列作为最后一个元素,所以thread-last
宏更为合适。
some->
和some->>
和上述两个宏基本相同,但是如果表达式的任意一步的结果是 nil,则计算结束。
thread-as
thread-as
宏as->
相比上两者,更加灵活:你为它提供一个名称,它将把各个连续形式的结果绑定到这个名称,以便下一步使用。
1 | (as-> {"a" [1 2 3 4]} <> |
该例子展开后如下:
1 | (let [<> {"a" [1 2 3 4]} |
条件式串行宏
cond->
和cond->>
除了每个形式都包含一个条件之外,和->
以及->>
基本相同,如果一个条件为false,则对应的形式将会跳过,但是对下一对形式继续串行求值(cond
则是在发现为真的判定后将立刻停止后续成对形式的求值,而cond->
会对每个条件求值)。
1 | (let [x 1 y 2] |
其等价描述为:
1 | (let [x 1 y 2] |