Clojure学习笔记
原文 http://bsr1983.iteye.com/blog/2175591
Clojure官方网站: http://clojure.org/
IntelliJ 插件地址 https://cursiveclojure.com/
Clojure是在JVM上重新实现的Lisp。
Clojure中的并发工具包和数据结构就是一项新技术。并发抽象层让程序员可以写出更加安全的多线程代码。它和Clojure的序列抽象层(对集合和数据结构上的不同看法)相结合,为开发人员提供了非常强大的工具箱。
Clojure认为值才是真正重要的概念。值可以是数字、字符串、向量、映射、集合,或其他任何东西。一旦创建,Clojure的值就不能再变了,因为它们是不可变的。
Clojure处理状态和内存的方式是它在名字和值之间创建了一个关联关系。这就是绑定,通过特殊形式(def)建立。Clojure中的特殊形式相当于Java的关键字。
(def)的句法是:
(def<名称> <值>)
在REPL中可以输入Clojure代码,也可以执行Clojure函数。它是个交互式环境,而且在前面得出的计算结果不会被丢掉。可以用它做探索式编程。
Clojure中没有可变状态,但有可以改变绑定值的符号。Clojure不是让“内存盒子”中的内容改变,而是让符号绑定到不同的不可变值上。在程序的生命期内,var可以指向不同的值。
可变状态和不同绑定两者之间的区别很微妙,但这个概念很重要,一定要掌握。要记住,可变状态是指盒子中的内容变了,而重新绑定是指在不同时间指向不同的盒子。
“定义函数”宏(defn)。宏是类Lisp语言的关键概念之一,其核心思想是内置结构和普通代码之间的区别应该尽可能小。
- (defn<符号><值?>):把符号绑到值上(如果有的话)。如果有必要创建与符号对应的var。
- (fn<名称>?[<参数>*]<表达式>*):返回带有特定参数的函数值,并把它们应用到表达式上。通常跟(def)相结合,变成形式(defn)
- (if<test><then><else>?):如果test的计算结果为true,计算then并产出其结果。否则计算else并产出其结果,当然,前提是else存在。
- (let[<绑定>*]<表达式>*):给局部名称分配别名值,并隐式定义一个作用域。使得在let作用域内的所有表达式都能获得该别名。
- (do<表达式>*):按顺序计算表达式的值,并产出最后一个结果
- (quote<形式>):照原样返回形式(不经计算)。它只能接受一个形式参数,其他的参数全都会被忽略。
- (var<符号>) :返回与符号对应的var(返回一个Clojure JVM对象,不是值)
(quote)以一种特殊的方式处理它的参数。具体来说就是它不会计算参数,所以第一个参数不是函数值也没有问题。
Clojure的向量(vector)跟数组类似,实际上,基本上可以把Clojure列表等同于Java的LinkedList,向量等同于ArrayList。向量可以用方括号表示。
(vec)形式以一个列表为参数,并用这个列表创建向量,而(vector)形式以多个独立符号为参数,并返回包含它们的向量。
函数(nth)有两个参数:集合和索引。它跟Java中的List接口的get()方法类似。可以用在向量和列表上,也可以用在Java集合甚至字符串(字符的集合)上。
Clojure也支持映射(map,相当于Java的HashMap),定义很简单:
{key 1 value1 key2 "value2"}
关于关键字,请记住下面这些知识点:
- Clojure的关键字是只有一个参数的函数,其参数必须是映射。
- 在映射上调用这个函数会返回映射里与该关键字函数对应的值。
- 关键字的使用遵循语法对称性规则,即(my-map:key)和(:key my-map)都是合法的。
- 关键字作为值使用时返回自身
- 关键字在使用之前无需声明或def。
- Clojure中的函数也是值,因此可以放在映射里当键用。
- 可以用逗号(但没必要)来分隔键值对,因为Clojure会把他们当做空格处理
- 除了关键字,其他符号也能用在映射里做键,但关键字太好用了们所以我们要特别提出来,你应该把它用在自己的代码中。
除了映射字面值,Clojure还有个(map)函数,它不像(list),(map)函数不会产生映射。而是对集合中的元素轮番应用其中的函数,并用返回的新值建立一个新集合。
Clojure也支持集(set),跟Java的HashSet很像。它的缩写形式是:
#{"apple" "pair" "peach"}
Clojure没有Java里那种意义的操作符,只能用函数:
(add 3 4)
也可以这样写
(+ 3 4)
Clojure的相等形式(相当于Java里的equals()和==)状况稍微有点复杂。Clojure有两个跟相等相关的形式:(=)和 (identical?)。注意它们的名字,这全都是因为Clojure不为操作符保留字符。另外,(=)也是等号,而不是赋值符号。
(+)是clojure.core命名空间下的函数,能够接受0到任意数目的参数,假如没有参数,则返回0。
Clojure中有两个值表示逻辑假:false和nil。其他全是逻辑真。
对于VM来说,Clojure函数是实现了clojure.lang.IFn的对象。
Schwartzian转换的基本思想是基于向量中的元素的某些属性對元素进行排序。排序所依据的属性值是通过在元素上调用键控函数确定的。
读取器宏
' 引号,展开为(quote),产生不进行计算的形式
; 注释,标记知道行尾的注释,就像Java里的//
\ 字符,产生一个字面字符
<p>@ 解引用,展开为(deref),接受var对象并返回对象中的值(跟(var)形式的操作相反)。在事务内存上下文中海油其他含义。 </p>^ 元数据,将一个元数据映射到附加对象上。
` 语法引用,经常用在宏定义中的引号形式,不太适合初学者
# 派发,有几种不同的子形式
#' 展开为(var)
#{} 创建一个集字面值
#() 创建匿名函数字面值,用在哪些使用(fn)太啰嗦的地方
#_ 跳过下一个形式。可以用#_(....多行...)来创建多行注释
#"<模式>" 创建一个正则表达式(作为java.util.regex.Pattern对象)
在自身的环境内”封装“一些值的函数称为闭包。
序列是Clojure的创新,实际上,用Clojure编程主要就是要思考怎么用序列解决特定的问题。
Clojure与Java中的集合与迭代器相对应的核心概念是序列(sequence),或者简称seq。它基本上是把两个Java类的一些特性集成到一个概念里。这样做的动机有三个:
- 更强健的迭代器,特别是对于多路算法而言。
- 不可变能力,可以安全地在函数间传递序列。
- 实现了懒序列的可能性。
(seq <coll>) 返回一个序列,作为所操作集合的”视图“
(frist <coll>) 返回集合的第一个元素,如有必要,先在其上调用(seq)。如果集合为nil,则返回nil
(rest <coll>) 返回从集合中去掉第一个元素后的到的新序列。如果集合为nil,则返回nil
(seq? <o>) 如果o是一个序列,则返回true(也就是实现了ISeq)
(cons <elt> <coll>) 在集合前面增加新元素,并返回由此得到的序列
(conj <coll> <elt>) 返回将新元素加到合适的一端(向量的尾端和列表的头)的新集合
(every? <pred-fn> <coll>) 如果(pred-fn)对集合中的每个元素都返回逻辑真,则返回true
列表是自身的序列,而向量不是。因此从理论上来来说,不能在向量上调用(rest)。而实际上是可以的,因为 (rest)在操作向量之前先在其上调用了(seq)。这是序列结构中普遍存在的属性:很多序列函数都会接受比序列更通用的对象,并在开始之前先调用 ( seq )。
Clojure函数有一个强大的特性,它天生就具备参数数量可变的能力,有时称为函数的变元(arity)。参数数量可变的函数称为变参函数(variadic)。
(defn const-fun-arity1
([] 0)
([x] 1)
([x & more] "more"))中的&表明这是该函数的变参版本。
(defn lenStr [y] (.length (.toString y)))中用到了形式(.toString)和(.length),这都是Java方法,它们是在Clojure对象上调用的。符号开始部分的句号,表示运行时应该在下一个参数上调用该名称的方法,底层是用(.)宏实现的。
所有用(def)或它的变体定义的Clojure值都被放在clojure.lang.Var实例中,它可以承载任何 java.lang.Object,所以任何可以在java.lang.Object调用的方法都可以在Clojure值上调用。另外一些跟Java交互的形式是用来调用静态方法的。
(System/getProperty "java.vm.version")
Clojure调用的本质
Clojure中的函数调用实际上是JVM的方法调用。JVM不能保证像Lisp语言(特别是Scheme)通常做的那样优化掉尾递归。JVM 上一些其他的Lisp方言觉得它们需要真正的尾递归,因此不准备把Lisp函数调用跟JVM方法调用完全等同起来。而Clojure完全以JVM为平台,甚至不惜违背通常的Lisp实践。
如果你想创建一个新的Java对象并在Clojure中操作它,用(new)形式就可以轻松做到。它还有个备选的缩写形式,在类名之后跟一个句号,可以归结为(.)宏的另一个用法。
Clojure有一个强大的宏(proxy),可以用它来创建扩展Java类(或实现接口)的Clojure对象。
(proxy)的一般形式是:
(proxy [<超类/接口>] [<args> <命名函数的实现>+])
第一个向量参数是这个代理类应该实现的接口。如果这个代理还要扩展Java类(如果可以的话,当然,只能扩展一个Java类),这个类名必须是向量中的第一个元素。
第二个向量参数包含传给超类构造方法的参数。这个向量经常是空的,并且如果(proxy)形式只是实现Java接口的话,那它肯定是空的。
这两个参数之后是一个或多个表示单个方法实现的形式,按接口的要求或超类指定的实现。
Clojure的类型系统跟Java高度一致。Clojure数据结构全是真正的Java集合,都实现了对应接口的所有必须部分。因为接口的可选部分一般都跟修改数据结构有关,而Clojure数据结构不可变,所以一般都没实现。
import clojure.lang.ISeq; import clojure.lang.StringSeq; /** * Created with IntelliJ IDEA. * User: billlee * Date: 2015/1/13 * Time: 16:04 * To change this template use File | Settings | File Templates. */ public class ClojureDemo { public static void main(String[] args) { ISeq seq= StringSeq.create("football"); while(seq!=null) { Object first=seq.first(); System.out.println("Seq:"+seq+" ;first:"+first); seq=seq.next(); } } }
Clojure的指导思想是默认把线程彼此隔开,这种实现并发安全的办法由来已久。假定”没有共享资源“的基线和采用不可变值使Clojure避开了很多Java所面临的问题,从而可以专注于为并发编程安全地共享状态的方法。
实际上,Clojure用不同的方法实现了不同的并发模型:未来式(future)、并行调用(pcall)、引用形式(ref)和代理(agent)。
第一个也是最明显的一个状态分享办法就是不分享。实际上,我们一直使用的Clojure结构var本质上是不可以共享的。如果两个不同线程继承了名字相同的var,并在线程里重新绑定了它,那绑定只在这些线程内部可见,绝不可能被其他线程共享。
有个简单的函数是(pcalls),可以接受数量可变的零参函数,让它们并发执行。它们在运行时管理的线程池上执行,并返回一个懒序列结果。试图访问序列中的任何还没完成的元素会导致访问线程被阻塞。
ref是Clojure在线程间共享状态的办法。它们基于运行时提供的一个模型,在这个模型中,状态的改变要能被多个线程见到。该模型在符号和值之间引入了一个额外的中间层。也就是说,符号绑定到值得引用上,而不是绑定到值上。这个系统基本上是事务化的,并且由Clojure运行时进行协调。
代理是Clojure中异步的、面向对象消息的并发原语。Clojure代理不是共享状态,而是属于另外一个线程的一点儿状态,但它会从另外一个线程中接收消息(以函数的形式)。
应用到代理上的函数在代理的线程上运行。这个线程是由Clojure运行时管理的,在一个程序员通常无法访问的线程池里。运行时还会保证代理中那些可以被外界看到的值是孤立的和原子的。这就是说用户代码只会见到状态修改之前或者之后的代理值。
涉及到的部分练习的代码:
(ns com.clojure.ClojureDemo) (def hello (fn [] "Hello world")) (hello) ;使用java中的toString()结合length()获取字符串长度 (defn lenStr [y] (.length (.toString y))) (defn schwartz [x f] (map #(nth %1 0) (sort-by #(nth %1 1) (map #(let [w %1] (list w (f w)))x)))) (schwartz ["sads" "21" "ssssewe" "22323" "223" "s"] lenStr) '(1 2 3 4 5) (quote (1 23 45 32 545)) (vector 1 2 3) (vec '(1 2 3 4 5)) [1 2 3 4 5 4 5 2] ["ssa" 2 "dsadsa" "321" 221] (nth '(1 2 3 "433" "rewr" "e33") 4) (def foo {"aaa" "111" "bbb" 22222}) (foo "aaa") (foo "bbb") (def martijn {:name "Martijn Verburg",:city "London",:area "Highbury"}) (:name martijn) (:city martijn) (def ben {:name "ben Evans",:city "London",:area "Holloway"}) (def authors [ben martijn]) (map ( fn [y] (:name y))) (map ( fn [y] (:name y)) authors) (+ 3 4) (defn add [x y] (+ x y)) (+ 2 3 4 56) (def list-int '(1 2 3 4)) (def vect-int (vec list-int)) (identical? list-int vect-int) (defn const-fun1 [y] 1) (defn iden-fun [y] y) (defn list-maker-fun [x f] (map (fn [z] (let [w z] (list w (f w))))x)) (list-maker-fun ["a"] const-fun1) (list-maker-fun ["a" "b"] const-fun1) (list-maker-fun [3 4 65] iden-fun) (schwartz [33 452 53 42 555] iden-fun) (defn like-for [counter] (loop [ctr counter] (println ctr) (if (< ctr 10) (recur (inc ctr)) ctr))) (like-for 100) (like-for 60) (like-for 6) (like-for 1) (defn like-for [counter] (loop [ctr counter] (println ctr) (if (< ctr 100) (recur (inc ctr)) ctr))) (like-for 99) (like-for 1) (defn adder [constToAdd] #(+ constToAdd %1)) (def plus2 (adder 2)) (plus2 222) (def plus100 (adder 100)) (plus100 22) (rest '(1 2 3)) (first '(1 2 43 54)) (rest [1 23 45]) (seq ()) (seq []) (seq '(1 2 3 4)) (seq [22 1 "re" "fdsw" "fdsf" "fdsffd"]) (cons 1 [2 34 43]) (defn next-big-n [n] (let [new-val (+ 1 n)] (lazy-seq ( cons new-val (next-big-n new-val) )))) (defn natural-k [k] (concat [k] (next-big-n k))) (take 10 (natural-k 3)) (defn const-fun-arity1 ([] 1) ([x] 1) ([x & more] 1)) (const-fun-arity1 1) (const-fun-arity1 1 2) (const-fun-arity1 1 2 3) (defn const-fun-arity1 ([] 0) ([x] 1) ([x & more] "more")) (const-fun-arity1) (const-fun-arity1 1 ) (const-fun-arity1 1 2 3) (System/getProperty "java.vm.version") (import '(java.util.concurrent CountDownLatch LinkedBlockingQueue)) (def cdl (new CountDownLatch 2)) (def lbq (LinkedBlockingQueue.)) (.getClass "test") (.getClass 2.3) (.getClass [12 34 534]) (.getClass '(1 2 3 4)) (.getClass (fn [] "Hello world")) (import '(java.util.concurrent Executors LinkedBlockingQueue TimeUnit)) (def stpe (Executors/newScheduledThreadPool 2)) (def lbq (LinkedBlockingQueue.)) (def msgRdr (proxy [Runnable] [] (run [] (.toString (.poll lbq))))) (def rdrHndl (.scheduleAtFixedRate stpe msgRdr 10 10 TimeUnit/MILLISECONDS)) (import '(java.util ArrayList LinkedList)) (.getClass (.iterator (ArrayList.))) (.getClass (.iterator (LinkedList.))) (def simple-future (future (do (println "hello world Line0") (Thread/sleep 10000) (println "we are the world Line1") (Thread/sleep 10000) (println "do the best Line 2")))) (defn wait-with-for [limit] (let [counter 1] (loop [ctr counter] (Thread/sleep 500) (println (str "Ctr=" ctr)) (if (< ctr limit) (recur (inc ctr)) ctr)))) (defn wait-1 [] (wait-with-for 1)) (defn wait-2 [] (wait-with-for 2)) (defn wait-3 [] (wait-with-for 3)) (def wait-seq (pcalls wait-1 wait-2 wait-3)) (first wait-seq) (first (next wait-seq)) (defn make-new-acc [account-name opening-balance] {:name account-name :bal opening-balance}) (defn loop-and-debit [account] (loop [acc account] (let [balance (:bal acc) my-name (:name acc)] (Thread/sleep 1) (if (> balance 0) (recur (make-new-acc my-name (dec balance))) acc)))) (loop-and-debit (make-new-acc "Ben" 600)) (defn make-new-acc [account-name opening-balance] (ref {:name account-name :bal opening-balance})) (defn alter-acc [acc new-name new-balance] (assoc acc :bal new-balance :name new-name)) (defn loop-and-debit [account] (loop [acc account] (let [balance (:bal @acc) my-name (:name @acc)] (Thread/sleep 1) (if (> balance 0) (recur (dosync (alter acc alter-acc my-name (dec balance)) acc)) acc) ))) (def my-acc (make-new-acc "Ben" 500)) (defn my-loop [] (let [the-acc my-acc] (loop-and-debit the-acc))) (pcalls my-loop my-loop my-loop my-loop my-loop) (defn wait-and-log [coll str-to-add] (do (Thread/sleep 10000) (let [my-coll (conj coll str-to-add)] (Thread/sleep 10000) (conj my-coll str-to-add)))) (def str-coll (agent [])) (send str-coll wait-and-log "some str") @str-coll