Clojure學習筆記

jopen 11年前發布 | 19K 次閱讀 Clojure JavaScript開發

原文  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對象,不是值)
  • </ul>

    (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會把他們當做空格處理
    • 除了關鍵字,其他符號也能用在映射里做鍵,但關鍵字太好用了們所以我們要特別提出來,你應該把它用在自己的代碼中。
    • </ul>

      除了映射字面值,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類的一些特性集成到一個概念里。這樣做的動機有三個:

      • 更強健的迭代器,特別是對于多路算法而言。
      • 不可變能力,可以安全地在函數間傳遞序列。
      • 實現了懶序列的可能性。
      • </ul>

        (seq <coll>)       返回一個序列,作為所操作集合的”視圖“

        </div>

        (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(); } } }</pre>
          </div>

          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</pre>
         本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
         轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
         本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!