在我们开始学习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 \a \b \c) "abc"
=>(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 ¶ ¶4] (print (+ para1 para2 ¶3 ¶4))) => (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
=> (print (#(str "our name are ",%1," ",%2) "lb" "cxh")) our name are lb cxh nil需要注意的是java里面的overload可以根据参数类型或参数个数来重载,但是clojure中只能根据参数个数来重载。不过clojure中的multimethods技术可以实现任意类型的重载(还没看到,后面再补充)