Clojure,这是什么鬼?一门基于JVM(现在也有基于.NET CLR的了:Clojure CLR) 的函数式编程语言。在JVM平台运行的时候,会被编译为JVM的字节码进行运算。。为什么要学它?其设计思想独特。有何先进独特之处?后面我会讲。
说实话,现在编程语言满天飞,哥也只是玩过C/C++/Basic/C#/javascript/Java/Python,,哥最喜欢的语言么?看平台了。Windows是C#,跨平台Java,脚本Python。其它的,比如: “最纯的函数式语言”Haskell、“天生擅长高并发的”Erlang,“当红辣子鸡的并发语言”Go,“上手最快的高并发语言”Node,“简约而不简单”的Scala.....,这些都是很棒的语言。哥今天来学一门独特的语言Clojure,首先因为它是LISP —— 一门富有传奇色彩的语言,其次,它小巧、简洁、稳定,是非常酷的语言,既继承了lisp的优美,也保留了java的实效,还具有高并发的特性。而且有个有名的应用实例,那就是Twitter Storm流式计算框架——开源实时Hadoop,是用Clojure语言写的。拿来玩玩,不选它选谁呢?
环境安装
哥今天是在一台win7的笔记本上来安装的(mac的兄弟此处可以鄙视我~),首先机器要安装好JDK7和maven并已配置好环境变量,然后Google搜索“clojure install”,下载与安装leiningen(这是什么鬼?Clojure的项目构建工具,可以自动给你搞定clojure项目打包依赖),设置环境变量,最后是安装IDE插件,例如:Emacs、Eclipse(至少Kepler)。OK,搞定!
使用 immutable 数据
这是Clojure在理念上与我们平常的Java或c#最大的区别,即使用不可改变的值,这儿的值可以是数字、字符串、向量、映射或集合。一旦创建,值不可改变。Clojure不是让“内存变量”的内容可变,而是让符号绑定到不同的不可变值上。
例如:( def hello ( fn [] "Hello, world!" ) )
这段Clojure代码把标识符hello绑定了一个值,这个值是一个函数(函数式编程意味着函数是一个值): (fn [] "Hello, world!"),不带参数,输出"Hello, workld!"。我们运行一下:(hello),输出Hello, workld!
现在我们让它重新绑定另外一个值:( defn hello ( fn [] "Get shit done!" ) )
这段Clojure代码把标识符hello绑定了另外一个值,这个值是一个函数: (fn [] "Get shit donw!"),不带参数,输出"Get shit done!"。
注意:与java等的变量赋值相比,区别是一个是变量内容变了,一个是值一旦创建,不可改变,符号重新绑定是指在不同时期指向不同的值。
使用 immutable 数据的好处是什么?区分identity和value,value不可变,给identity赋值新的value时都要经过语言内制定的function,由语言来保证一致性,让编写并发程序变得容易。这个并发的特性后面会讲到。
闭包(closure)
闭包是函数式编程中非常重要的特性,Clojure的闭包很类似javascript的闭包,举个栗子:
(defn double-op
[f]
(fn [& args]
(* 2 ( apply f args ))) )
(def double-add (double-op +))
上面代码的详细解释:第一行定义一个名为“double-op”函数,这个函数用一个形参f。这个形参f应该是一个函数,因为我们的函数体是一个用fn(fn是一个宏,可以理解为宏也是一种能够动态生成函数的方式,且功能上强大很多)定义的匿名函数。这个匿名函数可以接受一或者多个参数(形参名字args前的“&”表明了这一点)。这个匿名函数会通过传入的实参(也就是f的值)而完整化,并作为函数“double-op”的返回值。函数apply会将第一个实参(一般为一个函数)作用于其余的实参之上,也就是说调用第一个实参代表的函数,并将其余的实参作为其参数传入。使用apply的好处在于不必立刻在代码中填入传入“其余的实参”,而可以用引用名代替。这时,这些“其余的实参”可以被叫做预参数。倒数第二行代码定义了一个名为“double-add”的引用,这个引用返回一个函数。这个返回的函数是通过向函数“double-op”传入函数“+”而完整化后得出的。换句话说,我们在这里定义了一个名为“double-add”的函数。调用方法是:(double-add 5 6),输出 22(把所有“其余的参数”相加并乘以2)。
并发(Concurrency)
Java的状态模型从根本上来说是基于状态可变思想的,这直接导致并发代码的安全问题,所以OOP的java的并发编程非常复杂,只能靠悲观锁(locking),或CAS来解决。当然,OOP的真正优势在于对现实世界的建模,而不是数据处理。我们应该辩证的看待不同范式的编程语言,死磕一个必然会使思想禁锢,甚至编程灵感尽失。回到正题,Clojure的指导思想是把线程彼此隔开来实现线程安全,开发人员不用care线程调度,让Clojure来管理线程池。假设“没有共享资源”的基线和采用不可变值使Clojure避开了很多Java面临的问题。举例来说,Clojure的Ref并发模型使用STM机制,该模型在符号和值之间引入了一个额外的中间层,符号绑定到值的引用上,而不是直接绑定到值上,这个系统是事务化的,由Clojure运行时来进行协调,开发人员无需担忧任何锁问题。
STM机制比较抽象,举个栗子吧:银行转账的时候,客户的银行余额显然应该是可变的,而且肯定会有多个线程会对这个余额进行读写,Clojure对于这种情况提供了软件事务内存(Software Transactional Memory -- STM),STM的作用简单点说就是我们无法直接对状态进行读写, STM代理了我们对于状态的所有读写,当我们说要一个状态进行修改的时候,STM充当了余额状态与值的中间层 -- 也就是说多线程之间的协调由STM帮我们搞定了,自然不会有问题了。
以上是对STM的粗略解释,要深入研究建议去阅读R. Mark Volkmann的论文《Software Transactional Memory》。
下面是使用ref的一段代码,不解释了,自己去玩吧:
(import ' (java.util.concurrent Executors) )
;;(test-stm 10 10 10000)
;;-> (550000 550000 550000 550000 550000 550000 550000 550000 550000 550000)
(defn test-stm [nitems nthreads niters]
(let [refs (map ref ( repeat nitems 0 ))
pool (Executors/newFixedThreadPool nthreads )
tasks (map (fn [t]
(fn []
(dotimes [n niters]
(dosync
(doseq [r refs]
(alter r + 1 t ))) )))
(range nthreads ))]
(doseq [future (.invokeAll pool tasks)]
(.get future) )
(.shutdown pool)
(map deref refs)) )
可以看到比起冗长的Java,Clojure的语法非常简练,封装的很好,既继承了lisp的优美,也保留了java的实效,还具有高并发的特性。
CPU/网络I/O高并发
现在所谓的“高并发”很多是指网络I/O高并发,具体来说指的是单进程接受多少多少个连接,例如:Node.JS实现了这个,可以用很少的资源吃满I/O,这里的资源的重点自然是指内存和CPU。而Clojure的高并发完全不是指的这个,Clojure的STM高并发指的是CPU密集型的高并发,不是网络I/O, 这点不要搞错了。其网络I/O高并发完全依赖JVM,或是其他的NIO框架比如Netty或Mina什么的,所以Clojure 提供了相对更为正交的功能集合(STM,并发支持,独立的异步 I/O 库)
原文发布与微信公众号 rayisthinking