[置顶] clojure的一个计数器——a counter in clojure

写在开头的话:clojure中的很多语法现象都值得去思考。有时候稍微一点认知的错误,都容易导致“失之毫厘,差之千里”,这篇文章就是一个clojure变量counter(其实是不变量~immutable)引发的血案:( —佚名

阅读本文的内容可能要求你有的clojure相关知识:
基本语法
推荐网站

Are you ready, brave reader? Are you ready to meet your true destiny? Grab your best pair of parentheses: you’re about to embark on the journey of a lifetime! —braveClojure

推荐网站2
clojure风格指南

并发处理 1 推荐文章

本文不是从传统的角度来讲解如何clojure编程,而是写一个理解的过程(关于def 、let、binding等),如果你有同样的困惑,希望也能给你解惑。

1. 是什么变量?

我们项目团队的架构师leader给我们推荐并介绍clojure的例子讲到了一个变量,当时我没有注意,后来再看ppt的时候觉得有些困惑的地方。这个var是作为计数器counter,定义如下:

(def counter (let [tick (atom 0)] #(swap! tick inc)))

每次调用counter就能够实现一次计数的功能。

实验:调用counter的一些基本做法

boot.user=> (def counter
       #_=> (let [tick (atom 0)]
       #_=> #(swap! tick inc)))
#'boot.user/counter
boot.user=> (repeatedly 5 counter)
(1 2 3 4 5)
boot.user=> (counter)
6

看到这个的时候,我觉得很奇怪,为何counter就实现了计数的功能了呢。有以下几点疑惑:
1. 这里的let绑定,[tick (atom 0)]难道不是每次调用counter的时候都绑定么?这样就不是每次都将tick置0,还如何计数?
2. 为何使用atom?有何作用?
3. 为何返回一个匿名函数(#(swap! tick inc),这是一个匿名函数啦,表示将tick的值自增1,可是为何这么做?
萦绕在我脑袋里的这些问题比较混乱,现在想想有的疑惑也许根本就不是问题。(但有的也许会一直成为问题,生活总是这样,总有一些你解决不了的问题。)

我在网上看到很多计数器是用另外一种方式实现的,作为对比,摘录如下,姑且称之为计数器2:

(def counter (atom 0))
(defn inc-counter []
  (swap! counter inc))
(inc-counter)
(inc-counter)
(inc-counter)
(inc-counter)
(inc-counter)
@counter ; => 5

这个计数器2有些缺点,全局变量tick在函数外,很可能受到外界的干扰。对于计数器2,理解上没有任何疑惑,他的实现很简单,就是一个全局变量,然后每次计算:自增1。

是什么蒙蔽了眼睛?

Repeatedly

刚开始我还是个clojure小白,使用Repeatedly来调用counter,但其实却白得并不清楚Repeatedly对counter做了什么事,还以为是什么高大上的事,故而对自己的认知误区很不以为然。
那么什么是repeatedly?

(repeatedly f) (repeatedly n f)
Takes a function of no args, presumably with side effects, and
returns an infinite (or length n if supplied) lazy sequence of calls
to it

Repeatedly,也就是用一个数字n和一个函数做参数,然后调用n次函数,并返回一个 懒惰序列。
Repeatedly一般用于有副作用的函数,确实,如果没有副作用,不停地调用同一个函数还有什么趣味可言,那就真的只有dull。
~~这下,算是弄清楚了repeatedly的用法!

2.括号()

clojure编程中使用的最多的就是括号(),虽然天天写一堆括号,然而我对括号却不是那么了解呢。

;Clojure代码由一个个form组成, 即写在小括号里的由空格分开的一组语句。 ; Clojure解释器会把第一个元素当做一个函数或者宏来调用,其余的被认为是参数。

()就是一种调用,如果使用不当就会导致语法错误。
在这里我受蒙蔽的点就是不太理解def也可以拿去定义函数。

Creates and interns or locates a global var with the name of symbol and a
namespace of the value of the current namespace (ns). See
http://clojure.org/special_forms for more information.—def

()中的symbol类型必须继承clojure.lang.IFn接口。反向实验:


boot.user=> (def a 1)
#'boot.user/a
boot.user=> a
1
boot.user=> (a)

java.lang.ClassCastException: java.lang.Long cannot be cast to clojure.lang.IFn

public interface IFn
extends Callable, Runnable
IFn provides complete access to invoking any of Clojure’s APIs. You can also access any other library written in Clojure, after adding either its source or compiled form to the classpath.—–IFn接口

IFn接口实现了有invoke函数和apply函数等
clojure中的函数(我猜测必然都是继承了IFn接口的),不论是匿名函数,还是非匿名函数都是可以被括号()调用的,当然apply也是可以的;而像java.lang.Long这种类型的数据放在()中就不行。
另外放在()中的还可以是special form,def,let,binding,if,fn,apply,解耦等都是special form.

上面讲到了蒙蔽我眼睛的两个罪魁祸首,但其实我是在避重就轻,最重要的一个原因其实是工作中使用到了一个变量,用于记录某一个操作的错误原因,假设如下:

(def ^:dynamic *a* (atom {}))
(binding [*a* (atom {})]
    ...

上述代码可以用于并发,那么问题是什么呢?作为一个clojure初学者,我很迷惑为何这里的a每次能被重新赋值(所以能支持并发),而计数器却只有第一次有一个初始化的值,还得我怀疑这个代码的并发性能(对,怀疑了很久)。然后在我验证了这里的并发性确实没有问题的情况下,我知道一定是别的什么地方出现了问题。。。一定是别的什么地方

3. 扩展 let/atom/binding

作用域和STM

首先,我们都发现这里有一个作用域的问题。作用域一般分为全局和局部的,let是词法意义上的局部变量,binding也是局部变量,不过可以嵌入到内层的函数。全局变量,比如vars(前面提到了def的定义),agenet等,atom是全局的用作缓存的东西,clojure使用STM软件事务缓存来存储持久化的状态。STM的四种模式还值得好好分析,至于agent,那个论文真是讲的再好不过了。

这里再次引用def的定义,下面可能要用到:

def
Usage: (def symbol doc-string? init?)
Creates and interns a global var with the name
of symbol in the current namespace (ns) or locates such a var if
it already exists. If init is supplied, it is evaluated, and the
root binding of the var is set to the resulting value. If init is
not supplied, the root binding of the var is unaffected.

Let

let用于绑定局部变量(字面意义上的,一个binding-form)。

(let [bindings*] exprs*)
binding => binding-form init-expr
Evaluates the exprs in a lexical context in which the symbols in
the binding-forms are bound to their respective init-exprs or parts
therein.

注意:下面将介绍的Binding绑定的是已经存在的变量,叫做var-symbol(这个就是用def定义的,而且一般还需要使用^:dynamic),而let绑定的只是一个binding-form;expr是表达式,可以被evaluate的。

Let相关:If-let相当于结合了if和let的用法:首先绑定一个form,再判断form是否true,如果为true再执行一份语句,如果为false,执行另一份语句,像if那样。当然还有when-let等判断+let的形式。。在开发过程中,使用这些已经定义的api,可以大大简化代码。

binding

(binding bindings & body)
binding => var-symbol init-expr
Creates new bindings for the (already-existing) vars, with the
supplied initial values, executes the exprs in an implicit do, then
re-establishes the bindings that existed before. The new bindings
are made in parallel (unlike let); all init-exprs are evaluated
before the vars are bound to their new values.

The new bindings are made in parallel (unlike let)

上面一段引用的加粗的字体的那句话的含义表示binding支持并行,多个线程并发执行。
计数器不用binding只用let的原因是:
1. Let足够达到目的了。
2. Let的绑定是静态的,用一个词法作用域symbol tick“替换了”外部的tick(如果存在的话,比如计数器2的tick就不能影响到这个计数器,而计数器2很容易受到外界的干扰),swap!闭包查找变量tick,找到的就是这个作用域的值(是一个局部缓存)。
而在我感到困惑的代码里,用到的是binding和atom的结合,这是因为,atom是全局变量,binding是局部变量,这样能支持并发,也支持线程内缓存。

解决了上面的问题,现在终于可以回答这个问题了:Atom用在let里面会怎样?答:将atom放在let中,是为了防止外部的干扰。

解开迷雾—这个变量究竟是什么

(def counter (let [tick (atom 0)] #(swap! tick inc)))

def
Usage: (def symbol doc-string? init?)
Creates and interns a global var with the name
of symbol in the current namespace (ns) or locates such a var if
it already exists. If init is supplied, it is evaluated, and the
root binding of the var is set to the resulting value. If init is
not supplied, the root binding of the var is unaffected.

一句话,counter是一个无参数的有副作用的匿名函数。为什么呢?下面将具体分析这个变量定义:
symbol:counter
doc-string?:不存在
init?:

(let [tick (atom 0)] #(swap! tick inc))
   这个初始化的表达式init?会被evaluate.

这个var counter创建的时候,init先用let绑定一个atom 变量tick(常驻内存中的变量),tick的初始绑定值为0。atom变量常驻内存中,如下图所示:
[置顶] clojure的一个计数器——a counter in clojure_第1张图片
然后counter被绑定到匿名函数#(swap! tick inc)的地址。
当counter绑定完之后,每次调用counter :(counter) 解释器就会找到counter ,也就是匿名函数的那个地址,然后调用它,就相当于执行了匿名函数 #(swap! tick inc),也就是给tick的值加1.。然而,counter的初始化只有一次,没有重新def,虽然如此,tick的值也可以被修改:每次调用counter(counter)都会给常驻内存中的counter作用域的tick的值加1。
只是要注意这种情况真的是非常少见!!一般def不会再用let的。不过这又给我一种启示,那就是let和binding的这种绑定应该是无处不在的,非常灵活,不要过于局限。

let binding可以看作单纯限制作用域的地方,而不是一个需要开辟空间的存储位置。
反正在我看来,这个计数器怎么说都是一种很取巧的做法。

我根据自己的理解解决了这一个问题,毕竟是因为原来我有很多错误的认知(读者朋友们若是没有这样错误的认知,或许会笑话我)。若你和我犯过同样的错误,希望我写下的这些文字能够帮助到你。那天我看到javascript动画处理的时候将一个var作为dom元素的属性才不会引起混乱,我想和这里应该是同理的。总之通过分析这一个问题,我对相关问题都有了更深的认知:) —写在后面的话

参考

  1. Multi-core Parallelization in Clojure – a Case Study . ↩

你可能感兴趣的:(函数式编程,clojure,对语法的理解)