clojure java math_Clojure学习笔记 02.快速上手
这一节让我们来了解一下那些“使用Clojure编程必须要掌握的东西”。
其实Clojure(以及其他lisp系语言)的学习曲线还是比较陡峭的。学习Clojure之前必须要掌握一些基础的数据结构和算法知识。
同像性
其实Lisp系语言的语法并没有什么可说的,因为Lisp系语言的代码实际上是由Lisp数据构成的。换句话说,当我们编写Lisp代码时,我们事实上是在描述一个数据结构。这种特性被称作同像性。例如,当我们在Lisp中调用函数时,实际上我们是写下了一个列表结构:
(+1 1) ; 函数调用
;=> 2
(class‘(+1 1)) ; 查看(+ 1 1)的类型
;=> clojure.lang.PersistentList
可以看到,(+ 1 1)的类型实际上是一个PersistentList。
因此,学习Lisp的语法其实就是在学习Lisp的数据表示。
同向性带来的好处是我们可以像操作数据结构一样操作代码,这为lisp系语言带来了其他语言不可比拟的元编程能力。
形式(Form)
lisp的语法单元被称为形式。形式指那些能够被读取器读入的代码片段。诸如数值、字符串、列表以及其他复合结构都是形式。相对的,一个没有闭合的列表不是一个形式。
在Clojure中,有一类特殊形式,比如用于定义变量的def,它们是语言的基础组成部分。
数值类型
和绝大多数语言一样,Clojure中自然也有数值类型,对数值类型求值,当然也会返回这个数值本身(废话!)
除了常见的整型和浮点数外,Clojure还支持任意精度实数和任意精度整数:
(class0.0000000000001M) ; 任意精度实数
;=> java.math.BigDecimal ; 它的实际类型
(class999999999999999999N) ; 任意精度整数
;=> java.math.Bigint
除此之外,Clojure还支持“有理数”类型:
(/1 3)
;=> 1/3 ; 除法的结果以分数形式表示
相较于浮点数,有理数类型不会损失精度,但会带来一定的性能损失。
数值计算
Clojure的数值计算与普通的函数调用并无不同:
(+1 2) ;=> 3
(-2 1) ;=> 1
(*2 2) ;=> 4
(/4 2) ;=> 2
(*(+1 2) 3) ;=> 9
有趣的是,这些函数都是可以接受任意数量的参数的:
(+1 2 3 4) ;=> 10
(apply *[1 2 3 4]) ;=> 24
(-1) ;=> -1
(+) ;=> 0
(*) ;=> 1
数值比较
数值比较在Clojure中也是函数:
(>2 1) ; => true
(=1 1) ;=> true
这些函数也可以接受任意参数,因此,我们可以用>=函数判断一个数列是否为降序:
(defndesc? [nums] (apply >=nums)) ; 定义函数desc?
;=> #'user/desc?
(desc? [4 3 2 1])
;=> true
注:在Clojure中有一个命名约定:返回值为布尔类型的函数应该以问号结尾。
布尔值与nil
布尔值只包含true和false两个值。在Clojure中,nil相当于java中的null。Clojure的真值判断遵循一个简单的规则:false、nil为假,其余值都为真。
!注意:在Clojure中,空列表不为假
条件判断
Clojure中自然也有分支跳转,需要用到一个叫 if的特殊形式:
(iftestthen else?)
if十分简单,当test为真时返回then,为假时返回else(如果有的话)。注意,当test为假时,else是不会被求值的。
(if(=(+1 1) 2)
"Math still works today!"
(println"Never happens"))
;=> "Math still works today!"
与Java、C语言的if不同,Clojure的if是具有返回值的。因此其实它相当于其他语言的三元运算符。
if中的then块只能包含一个形式,如果需要执行多条语句可以使用when:
(when(=(+1 1) 2)
(println"print something")
"Math still works today!")
;; print something
;=> "Math still works today!"
注意:when没有else块,如果判断条件不满足,when会返回nil。
许多语言都有if-else if-else的语言结构,在Clojure中可以使用cond实现同样的效果:
(defnspeak [x]
(cond
(=x :dog) "Woof!Woof!Woof!"
(=x :cat) "Mew~"
(=x :repeater) *1 ; *1是REPL的特殊变量,表示上一个在REPL中求得的值
:else nil)) ; :else会被求值为真,因此当上述条件都不满足时就会返回nil
(speak :dog) ; => "Woof!Woof!Woof!"
(speak :repeater) ; => "Woof!Woof!Woof!"
(speak :cat) ; => “Mew~”
(speak :repeater) ; => “Mew~”
(speak :monkey) ; => nil
Clojure中也有类似switch的结构case,上述例子可以用case进行改写:
(defnspeak2 [x]
(case x
:dog "Woof! Woof! Woof!"
:cat "Mew~"
:repeater *1
nil)) ; 最后一行表示默认情况
符号与变量
Clojure中的符号类似其他语言中的标识符。除了能够使用字母、数字、下划线(注:符号不能以数字开头)以外,Clojure符号还能够使用一些特殊符号,如+、-、*、/、、.等等。注意,除号(/)和句点(.)常被用于命名空间。
Clojure中可以使用特殊形式def声明一个变量:
(defmyname "Khellendros")
;=> #'user/myname
myname
;=> "Khellendros"
变量的初始值不是必须的:
(defno-value)
no-value
#object[clojure.lang.Var$Unbound 0xcc0a548 "Unbound: #'user/no-value"]
使用def定义的变量都是全局变量,使用特殊形式let可以定义局部量,这些局部量只能在let范围内使用:
(let[myage 22, mygender :male]
(do(println"Name: " myname)
(println"Age: " myage)
(println"Gender: " mygender)))
;; Name: Khellendros
;; Age: 22
;; Gender: :male
;=> nil
myage
;=> CompilerException java.lang.RuntimeException: Unable to resolve symbol: myage in this context, compiling:(null:0:0)
关键字
关键字类似于符号,不同之处在于,符号通常都会引用其他事物(比如变量和函数名),而关键字仅仅代表它本身。关键字的命名规则与符号类似,但必须以冒号(:)开头,且数字可用紧跟着冒号。
:a-keywd
;=> :a-keywd
:1+!
;=> :1+!
关键字最常见的用法是充当关联结构(如map和set)的键值。在Python和JavaScript中,我们通常会用字符串充当键值,然而,比较两个字符串是相当低效的做法,关键字相当于是单例对象,只需要比较它们的内存地址就可以了,效率显然要高很多。此外,关键字也可以用于表示枚举值。
字符与字符串
在Clojure中,用反斜杠后面紧跟着一个字符表示字符类型:
(class\a)
;=> java.lang.Character
而字符串和java一样用双引号表示:
(class“Hello”)
;=> java.lang.String
字符串中间可以换行:
“Hello
world!”
;=> “Hello\nworld!”
str函数用以将一个值转化为字符串:
(str1) ;=> “1”
str也可以接受多个参数,此时它会将参数转化为字符串后拼接起来,并且会跳过nil
(str1 2 nil 3) ;=> “123”
正则表达式
在Clojure中可以使用井号(#)加字符串的方式定义一个正则表达式:
(defwords #”\w+”)
也可以使用re-pattern函数,不过注意要对正则表达式中的特殊字符用双反斜杠(\\)转义:
(defwords2 (re-pattern\\w+))
使用re-seq可以找到字符串中所有的匹配项:
(re-seqwords “aaa bbb|ccc,ddd”)
;=> (“aaa” “bbb” “ccc” “ddd”)
我们可以在正则表达式中添加分组:
(defmiddle-part #"\w+\-(\w+)\-\w+")
;=> #'user/middle-part
(re-seqmiddle-part "aaa-AAA-aaa bbb-BBB-bbb ccc-CCC-ccc")
;=> (["aaa-AAA-aaa" "AAA"] ["bbb-BBB-bbb" "BBB"] ["ccc-CCC-ccc" "CCC"])
此时re-seq将返回一个二维序列。
复合类型
Clojure提供了一组功能非常强大的容器,包括列表(list)、向量(vector)、映射表(map)、集合(set),由于篇幅有限这里仅对它们做一些简单的介绍。
所有这些Clojure容器都是不可变的,当我们使用诸如replace这样的函数改变容器内的元素时,我们将得到一个新的容器,而旧容器始终保持不变。
也许你会觉得这样的做法十分低效:“每次我要对容器进行更改时都需要把整个容器都复制一遍?”。其实恰恰相反,Clojure容器是十分高效的。不可变也意味着更容易实现“共享”——新旧容器之间可以共享大部分内容,当我们对容器做出更改时,通常只会生成一个新的根结点而不用拷贝整个容器。
列表(list)是Clojure(以及各种Lisp系语言)中最常见的结构,不过在Clojure中它的主要作用不是作为容器而是用来表示函数(或宏)调用。列表以一对括号表示,列表中的元素以空格或逗号分隔。
(+1 1)
;=> 2
在Clojure中,最常用的容器是向量(vector)。向量以一对中括号([ ])表示:
[1 2 3]
;=> [1 2 3]
向量的一大特点是它支持高效的随机访问。虽然向量的底层不是数组(array),但它进行随机访问的效率和数组相差无几。
(nth[1 2 3] 1) ; => 2
(get[1 2 3] 1) ; => 2
向量本身也可以当做函数使用,效果等同于nth:
([1 2 3] 1) ;=> 2
!但是注意,向量不能当成集合来使用,对向量使用contains?函数将永远返回true,哪怕该元素其实并不存在。
(contains?[1 2 3] 0) ;=> true
映射表(map)常用来表示一些相互关联的键值对,它使用花括号({ })来定义。
(defme {:name "Khellendros", :age 22, :gender :male})
;=> #’user/me
!注意:映射表中不能出现重复的键
使用get函数可以根据键查找值:
(getme :name)
;=> "Khellendros"
映射表也可以直接当成函数使用,效果等同于调用get函数:
(me :gender)
;=> :male
此外,关键字也能当成函数使用:
(:age me)
;=> 22
使用关键字作为函数对映射表进行查询,是Clojure的一种惯用法。
映射表的键可以是任意类型,同时同一个映射表的键的类型也可以各不相同:
(defmess-map {
:name "@#$$%",
[1 2 3] "aaa",
"?" 233 } )
;=> #'user/mess-map
(mess-map [1 2 3])
;=> "aaa"
(mess-map "?")
;=> 233
集合(set)相当于键和值相同的映射表。集合使用井号加花括号(#{ })表示,集合的内容不能重复。
(defimg-exts #{"jpg" "gif" "png" "bmp"})
;=> #'user/img-exts
集合通常用来判断其是否包含某个元素:
(contains?img-exts "jpg")
;=> true
集合自身也可以当做函数使用,效果等同于get
(img-exts "jpg")
;=> "jpg"
(img-exts "txt")
;=> nil
解构
复合类型可以进行解构。复合类型的构造可以看做是将多个量聚合成一个,而解构则是构造的逆过程,可以将复合解构拆解成多个量。
解构可以在许多地方发生,这里先以上面提到过的let为例:
(defnums [1 2 3])
;=> #’user/nums
(let[[a b c] nums] ; 解构nums
(+a b c))
;=> 6
可以看到,向量nums的第0、第1、第2项分别被绑定到了变量a、b、c上。
我们可以只取列表的前n项而忽略余项:
(defnatural-nums (iterate inc0)) ; 表示全体自然数
;=> #'user/natural-nums
(let[[a b c] natural-nums] (+a b c))
;=> 3
在这里natural-nums表示全体自然数的总集,因此它是一个无限长的序列,但因为我们只取其前三项,因此不会出现无限循环。
如果要只区第2项,忽略第0和第1项可以这么写:
(let[[_ _ a] natural-nums] a)
;=> 2
下划线(_)是一个合法的符号名,使用下划线忽略某些我们不关心的值是一种惯用法。注意这里下划线被绑定了两次,因此它最终的值是1而不是0。
:as命令可以将整个结构绑定到一个局部量上:
(let[[a b c :as all] nums] (str"Sum of: " all " = " (+a b c)))
;=> "Sum of: [1 2 3] = 6"
除了向量以外,映射表也可以进行绑定:
(let[{name:name, age :age} me] (str name" is " age " years old."))
:=> "Khellendros is 22 years old."
这种要把键名打两遍的做法略显繁琐,我们可以使用:keys命令进行简化:
(let[{:keys [nameage]} me] (str name“ is “ age “ years old.”))
:=> "Khellendros is 22 years old."
除了let以外,其他可以绑定局部量的位置都可以进行解构,包括但不限于函数参数,
loop,for等(见下文)。
函数
Clojure中有许多定义函数的方式,最常见的是使用宏defn:
(defnhello ;定义函数
"Say hello to someone." ;文档说明
[name] ;函数参数
(str"Hello, " name"!")) ;函数体
;=> #'user/hello
(hello "World")
;=> "Hello, World!"
函数可以有多个参数列表和函数体,此时各个参数列表的参数数量需要各不相同:
(defnvec-of
([a] [a])
([a b] [a b]))
;=> #'user/vec-of
(vec-of 1)
;=> [1]
(vec-of 1 2)
;=> [1 2]
在函数的参数声明处也可以对参数进行解构:
(defnthird [[_ _ x]] x)
;=> #'user/first3
(third natural-nums)
;=> 2
代码块
函数体只能包含一个形式,如果我们要执行多个表达式怎么办呢?使用特殊形式do可以解决这个问题:
(do
(println"first")
(println"second")
"not return"
"return")
;;first
;;second
;=> "return"
block会将最后一个形式的值当做返回值。
匿名函数
一些函数会使用一个回调函数作为参数,回调函数通常都只有一两行代码,如果我们懒得给它们起名字,可以使用匿名函数。匿名函数使用fn定义:
(defdouble-n (fn[n] (*n 2)))
;=> #’user/double-n
(double-n 10)
;=> 20
匿名函数还有一种简写形式,称作原位函数:
(defdouble-n-2 #(*% 2))
;=> #’user/double-n-2
(double-n-2 10)
;=> 20
原位函数使用井号加括号(#( ))定义,其中%[n]表示第n个函数参数,%1表示第一个参数,%2表示第二个……以此类推。单独一个%等价于%1
递归
Clojure不提供类似java的while和for循环,需要进行迭代时可以使用递归。在Clojure中可以使用特殊形式recur进行尾递归:
(defncount-down [n]
(when(pos?n) ; 如果n是正数就继续执行,否则返回nil
(printlnn) ; 输出n
(recur (decn)))) ; 递减n,然后递归调用count-down
;=> #'user/count-down
(count-down 10)
;;10
;;9
;;8
;;…
;;1
;=> nil
recur类似于C语言的goto语句,“跳转点”默认为recur所在的函数开始处,我们也可以用特殊形式loop自定义跳转点:
(defncount[start end]
(loop[n start, end end]
(when(
(printlnn)
(recur (incn) end))))
;=> #'user/count
(count0 10)
;; 0
;; 1
;; 2
;; …
;; 9
;=> nil
在loop中,我们绑定了两个局部量:n和end,当recur被调用时,他会将参数传递给loop绑定的变量。
注意:recur只能放置在一个函数或loop的出口处
遍历
虽然没有传统意义上的while和for循环,但是Clojure提供了类似java的foreach循环的设施——doseq和for。为了方便理解,我们会通过对比java代码来对它们进行说明。
doseq主要用于产生副作用(比如输出到控制台)。我们通过一个实际的例子来讲解一下doseq的用法:
(doseq[n [1 2 3]] ; 将列表[1 2 3]中的每一个元素依次绑定到n
(printlnn))
;; 1
;; 2
;; 3
;=> nil
下面是功能相同的java代码:
List nums =Arrays.asList(1, 2, 3);
for(intnum : nums) {
System.out.println(num);
}
doseq还能对多个结构进行遍历:
(doseq[m [1 2], n [3 4]]
(println
(strm " + " n " = " (+m n))))
;;1 + 3 = 4
;;1 + 4 = 5
;;2 + 3 = 5
;;2 + 4 = 6
;=> nil
上述例子清晰的展示了doseq是如何对两个向量进行遍历的。以下是等效的java代码:
List nums1 = Arrays.asList(1, 2);
List nums2 = Arrays.asList(3, 4);
for (int m : nums1) {
for (int n : nums2) {
String tmp = m + " + " + n + " = " + (m + n);
System.out.println(tmp);
}
}
for的功能比doseq更为强大,它拥有遍历、过滤、变换等多种功能。
; 求笛卡尔积
(for[m [1 2], n [\a \b]] [m n])
;=> ([1 \a] [1 \b] [2 \a] [2 \b])
; 变换
(for[num [1 2 3 4]] (incnum))
;=> (2 3 4 5)
;过滤出偶数
(for[n (range0 10) :when (even? n)] n)
;=> (0 2 4 6 8)
遍历映射表
对映射表进行遍历时,会将映射表转化为二维序列:
(seqme)
([:name "Khellendros"] [:age 22] [:gender :male])
因此我们可以像操作普通二维序列一样遍历映射表。
需要注意的是,Clojure映射表的默认实现是哈希表,因此元素的遍历顺序是无法预期的。
与Java互操作
在Clojure中可以使用已有的java类库,包括调用静态方法,构造类实例对象,调用方法,读取、设置字段值等。
调用静态方法/静态字段
可以用 (类名/静态方法名 [方法参数…])的形式调用静态方法和静态字段:
(Math/PI)
;=> 3.141592653589793
(Math/abs -1)
;=> 1
构造实例对象
(类名. [构造方法参数…]) 或者 (new 类名 [构造方法参数…])可以构造实例对象:
(defnums (java.util.ArrayList.))
;(def nums (new java.util.ArrayList))
;=> #’user/nums
nums
;=> []
方法调用
使用 (.方法名 对象 [方法参数…])调用方法:
(.add nums 1)
;=> true
nums
;=> [1]
读取、设置字段
读取字段的方法是 (.-字段名 对象):
(defpoint (java.awt.Point. 0 1))
;=> #’user/point
(.-x point)
;=> 0
使用set!函数可以设置字段值。
(set! (.-x point) 2)
;=> 2
(.-x point)
;=> 2
注:在Clojure中,以!结尾的函数往往意味着其会带来副作用(比如set!会改变对象的属性)。我们应该审慎的使用这些函数。
小试牛刀:index-of函数
我们上面提到contains?函数对向量无效,我们不妨自己实现一个功能类似的函数index-of。
index-of函数使用起来应该是这样的:
(index-of [1 1 4 5 1 4] 4)
;=> (2 5)
(index-of [1 1 4 5 1 4] 0)
;=> ()
如果序列内包含我们想要查找的目标,index-of会返回由所有匹配项的下表组成的序列。如果没有找到则返回空序列。
首先,我们需要将列表项与其对应的下标关联在一起。还记得我们之前定义的natural-nums吗?把它和向量结合起来就可以了:
(defnindexed-vec [vec]
(map vectornatural-nums vec))
;=> #’user/indexed-vec
(indexed-vec [\a \b \c \d])
;=> ([0 \a] [1 \b] [2 \c] [3 \d])
map函数用于将一个函数应用到一个序列的每一项上,例如:
(map inc[1 2 3])
;=> (2 3 4)
如果传递给map两个序列,那么它就可以通过一个二元函数将两个序列结合起来:
(map +[1 2 3] [3 2 1])
;=> (4 4 4)
同理,如果传入3个序列就要使用一个三元函数,以此类推。如果两个序列的长度不一致,较长的序列会被“截断”。
而vector函数的作用自然是构造一个向量,因此,(map vector natural-nums vec)就会把一个自然数序列(代表下标)和vec像拉链一样“拉”在一起。现在向量已经和下标关联起来了,我们就可以使用for简单的实现index-of了。
(defnindex-of [vec item]
(for[[indexvalue] (indexed-vec vec) :when (=value item)] index) )
;=> #’user/index-of
(index-of [1 1 4 5 1 4] 4)
;=> (2 5)
(index-of [1 1 4 5 1 4] 0)
;=> ()
更进一步?
index-of工作的很好,但还不够通用。如果对映射表、集合都能用通用的接口进行调用就好了。
pos 函数(通用版本的index-of)使用起来应该是这样的:
(pos [1 2 3 1] 1) ;=> (0 3)
(pos {:a 1, :b 2, :c 1} 1) ;=>(:a :c)
(pos #{1 2 3} 0) ;=> ()
要做到这一点,首先我们需要定义一个通用版本的indexed:
(defnindexed [xs]
(cond
(map?xs) (seqxs)
(set? xs) (seqxs)
:else (indexed-vec xs)))
pos相较于index-of只是把indexed-vec换成了indexed:
(defnpos [xs item]
(for[[indexvalue] (indexed xs) :when (=item value)] index))
再进一步,我们完全可以把item参数换成一个判断函数。来看看这个最终版本的pos-if吧:
(defnpos-if [xs pred]
(for[[indexvalue] (indexed xs) :when (pred value)] index))
;=> #’user/pos-if
(pos-if [1 2 3 1] #(>%1 1))
;=> (1 2)
clojure java math_Clojure学习笔记 02.快速上手相关推荐
- [原创]java WEB学习笔记02:javaWeb开发的目录结构
本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...
- Python学习笔记---day02快速上手
day02快速上手 课程目标:学习Python最基础的语法知识,可以用代码快速实现一些简单的功能 课程概要: 初识编码(密码本) 编程体验 输出 初识数据类型 变量 注释 输入 条件语句 1. 编码 ...
- JavaEE学习笔记-SpringBoot快速上手、热部署、乱码问题、部分注解解释
SpringBoot快速上手 一.快速创建SpringBoot应用 1.1利用IDEA提供的Spring Initializr创建Spring Boot应用 1.2Spring Boot生成的项目结构 ...
- oracle 快速上手,Oracle学习笔记:快速上手
开启服务:先开启Lisenterner服务 再开启ORCL服务 oracle登录 用户: - 管理员 sysDBA - 无密码 - 系统用户 system - manager - 普通用户 scott ...
- Java Web学习笔记02:在Intellij里创建Web项目
文章目录 一.利用Intellij创建Web项目 1.创建Java项目HelloWorld 2.添加Web服务器配置
- 2019年Java Web学习笔记目录
Java Web学习笔记目录 1.Java Web学习笔记01:动态网站初体验 2.Java Web学习笔记02:在Intellij里创建Web项目 3.Java Web学习笔记03:JSP元素 4. ...
- Servlet和HTTP请求协议-学习笔记02【Servlet_体系结构与urlpartten配置、HTTP请求协议】
Java后端 学习路线 笔记汇总表[黑马程序员] Servlet和HTTP请求协议-学习笔记01[Servlet_快速入门-生命周期方法.Servlet_3.0注解配置.IDEA与tomcat相关配置 ...
- XML学习笔记02【xml_解析】
Java后端 学习路线 笔记汇总表[黑马程序员] XML学习笔记01[xml_基础.xml_约束][day01] XML学习笔记02[xml_解析][day01] 目录 03 xml_解析 xml_解 ...
- Bootstrap学习笔记02【全局CSS样式、组件和插件、案例_黑马旅游网_首页】
Java后端 学习路线 笔记汇总表[黑马程序员] Bootstrap学习笔记01[快速入门.栅格布局][day01] Bootstrap学习笔记02[全局CSS样式.组件和插件.案例_黑马旅游网][d ...
最新文章
- 一阶暂态电路三要素法和三种响应
- Mac解决终端显示乱码
- 14条Yahoo(雅虎)十四条优化原则【转】
- java json jar包_jsonobject jar包下载
- 使用javascript模拟常见数据结构(四)
- 处理入参_看看优秀的程序员是如何处理NPE的
- 从hadoop-0.20.2升级到hadoop-1.0.3
- (35)FPGA面试题FPGA工程师努力的方向
- 今天有个销售员在问我:“自己每天都有在学习,但是为什么感觉没什么用,进步不大。”
- java 小球运动轨迹_java怎么实现小球的运动轨迹
- 开源漫画服务器Mango
- 解决阿里云ECS服务器下载 git 资源慢的问题
- 维基解密再爆猛料:CIA利用漏洞入侵全球数十亿个人电子设备
- 《利用Python进行数据分析》第七章——数据清洗与准备
- 网络 — MB/s、Mb/s、Mbps、Mbit/s、Kbps
- Python之解决tkinter.PhotoImage不显示图片的问题 Python3
- 性能之巅:常用性能分析方法
- python数据可视化之Matplotlib
- android仿今日头条个人中心页面
- 计算机网络-常用英文简写大全