clojure學習筆記(更新中)

在我們開始學習clojure語言之前,先選一個自己喜歡的ide吧,本人在嘗試intellij失敗後果斷回到了eclipse的懷抱中(對於有強迫症的同學,可以搜下xumingming大俠的intellij leiningen的文章)。插件的安裝步驟就不寫了,在eclipse marketplace 中搜索counterclockwise安裝重啓即可。

File->New->other->clojure project

創建我們第一個clojure文件 helloworld.clj


core.clj是我們創建clojure項目時eclipse給我們創建的事例。

開始寫helloworld了

在我們的helloworld.clj中寫

(print "helloworld")

然後按F11運行,這時候console會打印出helloworld,同時eclipse爲我們打開了一個新的視圖:REPL

REPL:Read-Eval-Print Loop 交互式編程環境。如果你會用Python,那麼這東西一定不陌生,只是現在才知道它叫這名字。

現在我們既能在helloworld.clj中寫代碼然後運行查看結果,又能直接在REPL中輸入我們的代碼了,看看在REPL中的helloworld吧

在REPL中運行時,需要將光標定位在行末再敲回車。查看歷史command:Ctrl+方向鍵


clojure支持強類型和動態類型,clojure有一個基本構成部分,稱作形式(form),form可以看作語法的一部分,布爾值、字符、字符串、集合(set)、映射(map)、向量(vector)這些都是form。從表面看form就是clojure的數據類型,我將form理解爲clojure內置操作基本類型的函數。

字符和字符串
表示一個字符使用符號\
=>\a
\a
使用str函數可將字符拼接成字符串
=>(str \a \b \c)
"abc"
clojure中字符串使用雙引號括起來,並使用C語言風格的轉義字符
=>(println "hello\nworld")
hello
world
nil

使用str函數將其他類型數據轉換成字符串,如果目標是Java類型,str會調用底層的toString()方法。該函數可接受多個參數,並且還能拼接非字符串。

=>(str 1)
"1"

字符和字符串是不相等的,好比java中char和String。

=> (= "a" \a)
false

布爾值和表達式

clojure支持強類型和動態類型,動態類型意味着類型在運行時求值。

clojure中的類型與java類型是統一的,可以使用class函數獲得其底層類型

=> (class true)
java.lang.Boolean
=> (class True)
CompilerException java.lang.RuntimeException: Unable to resolve symbol: True in this context, compiling:(NO_SOURCE_PATH:1:1) 

來看看if語句的使用

=> (if true (print "True it is"))
True it is
nil

帶上else的if:

=> (if false
   (print "True it is") (print "False it is"))
False it is
nil

從上面代碼可以看出,clojure是可以自動識別多行的,檢測的是()的對應。if函數只能接收一個或兩個參數,如果超過就會報錯:

=> (if true
   (print "True it is") (print "False it is") (print "11"))
CompilerException java.lang.RuntimeException: Too many arguments to if, compiling:(NO_SOURCE_PATH:1:1) 

list、vector、set、map

在學習集合、序列之前,我們先來看看他們的一些特殊成員及他們的常用操作,最後再來總結他們的共性。

list

列表list是元素的有序集合,對比java,我們可以看似Linkedlist。對錶頭表尾的操作是非常高效的。但是隨機訪問效率會差很多。

列表的兩種表示:

=> (list 1 2 3)
(1 2 3)
=> '(1 2 3)
(1 2 3)

列表的主要操作有四個:first(頭元素)、rest(除頭部以外的列表)、last(最後一個元素)、cons(將元素添加到頭部):

=> (first '("a" "b" "c"))
"a"
=> (last '("a" "b" "c"))
"c"
=> (rest '("a" "b" "c"))
("b" "c")
=> (cons "d" '("a" "b" "c"))
("d" "a" "b" "c")'

vector

向量也是元素的有序集合。對比java,可以看似ArrayList。它對最後一個的元素的操作或訪問是非常高效的。vector對於以索引的方式訪問某個元素或者修改某個元素來說非常高效,函數定義的時候指定參數列表用的就是vector。通過索引訪問元素效率非常高(nth方法)。

向量的兩種表示:

=> (vector "a" "b" "c")
["a" "b" "c"]
=> ["a" "b" "c"]
["a" "b" "c"]

下面是vector的常見函數

=> (first ["a" "b" "c"])
"a"
=> (last ["a" "b" "c"])
"c"
=> ( nth ["a" "b" "c"] 2)
"c"
=> ( nth ["a" "b" "c"] 4 "default")
"default"
=> (get ["a" "b" "c"] 4 "default")
"default"

get函數和nth很相似。他們都接收一個可選的默認值參數:如果給定的索引超出邊界,那麼會返回這個默認值。如果沒有指定默認值而索引又超出邊界,get函數會返回nil,nth函數會拋出一個異常。

向量也是函數,取下標爲參數:

=> (["a" "b" "c"] 2)
"c"

也可以合併兩個vector:

=> (concat ["a" "b" "c"] ["d" "e" "f"])
("a" "b" "c" "d" "e" "f")

這裏需要注意的是:合併後的返回值類型是列表而不是向量。許多返回集合的函數都返回序列這個抽象類型(很像java中的iterator思想)。

set

set集合是元素的無序集合,不包含重複元素(和我們java中的set集合相似)。

set的表示:

=> #{"a" "b" "c"}
#{"a" "b" "c"}

我們給它們個變量名myset再來操作它們:

=> (def myset #{"a" "b" "c"})
=> (count myset)
3

如果要合併set,可以用上文提到的concat函數,不過它返回的是序列。也可以用set自己的方法union,返回合併後的set。

=> (clojure.set/union #{"a" "b" "c"} #{"d" "e" "f"})
#{"a" "b" "c" "d" "e" "f"}

求差集(第一個set爲主導):

=> (clojure.set/difference #{"a" "b" "c"} #{"a" "b" "f"})
#{"c"}

同向量vector一樣,set不僅是集合也是函數。該函數可以判斷元素是否屬於該set的成員,如果屬於返回該成員,不屬於返回nil:

=> (#{"a" "b" "c"} "a")
"a"
=> (#{"a" "b" "c"} "d")
nil

判斷set是否包含某個元素還有個方法就是:contains? ,該函數返回布爾值。

=> (contains? #{"a" "b" "c"} "d")
false

在clojure.set命名空間裏還有一些其他函數:index,intersection,join,mapinvert,project,rename,rename-keys,select等。其中有些操作的對象是map,但返回的是set。

Map

和其他語言一樣map就是鍵值對集合。鍵和值可以爲任意對象。也是無序的。

下面是Map的表示,其中逗號“,”是可選的,解析的時候會被當作空格處理。

=> {"a" :1,"b":2,"c":3}
{"a" :1, "b" :2, "c" :3}

如果我們分行來顯示,逗號就沒必要了:

=>(def person{
            :name "littlered"
            :age 25
            :city "kunming"})
=> (person :name)
"littlered"

可以使用merge函數合併兩個map:

=> (merge {"a" :1,"b":2,"c":3} {"d":4,"e":5,"f":5})
{"f" :5, "e" :5, "d" :4, "a" :1, "b" :2, "c" :3}

集合

clojure提供一下集合類型:list、vector、set、map。雖然clojure中可以使用java的所有集合類型,但是一般不會這樣做,因爲clojure自帶的集合類型更適合函數式編程。

對於clojure集合注意以下幾點:

1) clojure集合是不可修改的。一旦集合產生之後,就不能從集合裏刪除/添加元素。所以clojure沒有提供對集合進行修改的函數。如果我們要給一個集合進行修改,其實是在創建一個新的集合(共享舊的集合內存)。

2) clojure集合是異源的。集合中可以裝任何東西,它們的類型不必相同。

3) clojure集合是持久的。持久意味着當一個新的版本產生後,舊的版本還是在的,clojure以一種非常高效、共享內存的方式來實現,比如給一個map添加一個新元素,新的map將共享舊map的內存和新元素的內存組成。

下面是集合的一些常用操作:

=> (count [1 2 3 4])
4
=> (reverse [1 2 3 4])
(4 3 2 1)

conj函數是conjoin的縮寫,添加一個元素到集合裏去。添加的位置取決具體的集合。

//todo

map

apply

序列

序列是與具體實現無關的抽象層。序列封裝了所有clojure集合(list、vector、set、map)、字符串、甚至包括文件系統結構(流、目錄)。一般來說,只要支持函數first、rest、cons,就可以用序列封裝起來。

1)測試

如果要測試序列,可以用判定函數。

=> (every? number? [1 2 3 :four])
false

關鍵字以冒號打頭,被用來當作唯一標示符,通常用在map裏面 (比如:red, :green和 :blue)。

2)修改序列

序列函數庫中包含一系列用於以各種方式轉換序列的函數。比如filter函數:

取出長度大於4的單詞:

=> (def words ["luke" "chewie" "han" "lando"])
   (filter (fn [word] (> (count word) 4)) words)
#'littlered.helloworld/words
("chewie" "lando")

上面代碼中內部的(fn [word] (> (count word) 4)) 這是一個匿名函數。我們也可以將它分解開:

=> (def words ["luke" "chewie" "han" "lando"])
=> (defn foo [word] (> (count word) 4))
#'littlered.helloworld/foo
=> (filter foo words)
("chewie" "lando")

定義函數

之前我們已經看見過很多次函數了,這裏系統的總結下:
一)使用defn宏用來定義一個函數

注意如果你經常用python,會很習慣的敲def,在clojure中def的作用是:在當前命名空間裏定義(或重定義)一個var(在定義的時候你可以給它賦值或者不賦值):

=> (def p "hello")
=> (print p)
hello
nil
二)函數的參數可以是不定的,可選的參數必須放在最後面,通過&符號把不定參數放在一個list中:
=> (defn defntest [para1 para2] (print (+ para1 para2)))
=> (defntest 1 2)
3
nil
=> (defn defntest [para1 para2 &para &para4] (print (+ para1 para2 &para3 &para4)))
=> (defntest 1 2 3 4)
10
nil

三)函數定義可以包含多個參數列表以及對應的方法體,每個參數列表必須對應不同個數的參數。

=> (defn defntest "這是定義函數可選的註釋,會自動生成doc"
     ([] (print "default para"))
     ([para1] (print "this is para1 func"))
     ([para1 para2](print "this is two paras func"))
     )
=> (defntest)
default para
nil
=> (defntest "hello")
this is para1 func
nil
=> (defntest "hello" "word")
this is two paras func
nil
匿名函數

前文中已經提到了fn函數,這裏再詳細介紹下。匿名函數通常被當作參數傳遞給其他有名函數,對於那些只在一個地方使用的函數比較有用。

匿名函數的兩種表示:

=> (print ((fn [name] (str "my name is ",name)) "lb"))
my name is lb
nil
=> (print (#(str "my name is ",%) "lb"))
my name is lb
nil

通過fn定義的匿名函數可以包含任意個數的表達式;通過#()定義的匿名函數只能包含一個表達式,#()中的%代表參數,如果有多個參數用%1 %2 ...表示:

=> (print (#(str "our name are ",%1," ",%2) "lb" "cxh"))
our name are lb cxh
nil
需要注意的是java裏面的overload可以根據參數類型或參數個數來重載,但是clojure中只能根據參數個數來重載。不過clojure中的multimethods技術可以實現任意類型的重載(還沒看到,後面再補充)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章