Clojure学习笔记

jopen 10年前

原文  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