Bio 
Find purpose, the means will follow
John Wang's Blog
 
May 7, 2013

Clojure 线性(箭头)操作符

学习Clojure过程中发现,那两个减号和大于号构成的箭头运算符(->->>)太好用了,大爱。

语法图示

单箭头 Thread-first

->操作符会把其参数form迭代式地依次插入到相邻的下个一个form中作为该form的第一个参数。这就好像把这些form串起来了,即线性化(Threading)。 由于它总是把前一个参数作为接下来的form的第一个参数插入,这种操作也叫 thread-first 。 如下图所示:

thread first

上图中的单箭头后面有顺序的三个form。它们经过单箭头的处理后会变成下图所示代码结构:

thread first result

注意,如果在->的参数序列中存在非form的元素,则->会先把它转化成一个form 然后再把它们串起来。 这对下面提到的双箭头操作符也是适用的。

比如下面三行代码就是等价的:它们得到的结果都是 (5 4 2 3)。 请注意前两行对rest使用上的不同。

1
2
3
(-> [1 2 3] rest (conj 4 5))
(-> [1 2 3] (rest) (conj 4 5))
(conj (rest [1 2 3]) 4 5)

其中第一行代码由->操作符串起了[1 2 3]rest(conj 4 5)三个参数。其中第一个为一 vector 数据,第二个为一函数,第三个为一个简单的form。

双箭头 Thread-last

双箭头操作符与单箭头操作符类似,不过它是把前一个参数form做为最后一个参数插入到接下来的form中的。所以它又叫 thread-last 。 如下图所示:

thread last

上图中代码等价于下面这个:

thread last result

进一步解释

->->>其实都是用clojure写的 (macro)。 宏是Lisp最亮丽的特色之一。它允许开发人员重新塑造这一语言本身,即为语言定义新的语法。 这一令其他开发人员匪夷所思的特性,让Lisp程序员做起来却是小菜一碟。这全因其具备宏编程的能力。

比如上面我们的代码示例中, Thread-first宏把原本通过嵌套括弧逐层调用的语法、即(conj (rest [1 2 3]) 4 5),改变成了顺序方式调用的语法、即(-> [1 2 3] rest (conj 4 5))

事实上,clojure语言本身很多语法元素都是用宏实现的。比如我们熟悉的循环操作语句when,就是用ifdo语句组装出来的。

而且你还能查看某个宏到底是如何通过原始clojure语法组装的。 办法是利用macroexpand函数。试试在REPL中输入下面这行代码:

1
(macroexpand '(-> [1 2 3] rest (conj 4 5)))

我们把前述代码示例中的第一行传给了macroexpand函数。你会看到执行结果正是其中的第三行代码,即(conj (rest [1 2 3]) 4 5)。 注意必须给macroexpand的参数form前加单引号已防止该form先被执行,否则macroexpand的实际参数将是该form的执行结果而非该form本身。

好处

除了代码可读性更强外,我觉得线性操作符最大的好处是它在鼓励函数化编程风格。

把原来嵌套调用的参数form转化成顺序的处理,这令你很容易把这个由form构成的序列看成一个由多个“处理”构成的数据加工管道: 第一个参数是要处理的数据,它顺序地通过管道中的每个环节(即一个“处理”),最后被加工成我们需要的值。 如下面代码所示:

1
(-> my-milk (pour-into cup) (mix-with sugar) drink digest) ; result: pee

这一思考模式会鼓励对“处理”的抽象,并形成独立的程序单元(如(mix-with sugar)drink等),同时把这些单元同“数据”(如my-milk)分离出来。 这种分离强化了程序的无状态性,使得程序的函数风格更纯粹、从而函数化程序的优势能更充分发挥出来,包括更强的并行性特性、更高的可测试性等。


分享到:



读者留言 (0)

()