Clojure Programming读书笔记(0)

Down the Rabbit Hole

 

1. REPL is short for Read, Eval, Print and Loop
2. Clojure代码都是基于表达式(expressions)的,这些表达式基于以下规则:
  • list,例如(+ 1 2),代表着调用,表达式的值就是调用的返回结果。list中第一个值相当于操作符,剩下的都是参数
  • 符号,比如+或inc函数,会将他们在当前域的值拿来计算,这些符号可以是函数、本地变量、Java类、宏或special form
  • 所有其他该是什么就是什么字面量
3. 在Clojure中,code is data is code, 关键在于你让它怎么看,好好理解
4. Clojure的数据读入(比如REPL读入)完全依赖于一个叫read的函数,它还有一些变种比如read-string接受一个字符串参数
5. 标量字面量是reader读入的非集合的值,很多应该是已经很熟悉了:
  • String就是"hello"这样的
  • 布尔就是true和false
  • nil对应着Java里的null
  • 字符也和Java类似用一个backslash(\)+字符表示,同时支持\u00ff和\o41这样的unicode(\u)和八进制(\o)写法,此外还有些特殊的字符\space、\newline、\formfeed、\return、\backspace、\tab
  • 关键字(keyword)被解析成他们自身,他们以一个colon(:)开头,常被用作map和record中的访问器 (约等于key吧),当我们定义了一个map,(def person {:name "Xiaoming"}),就可以通过(:name person)来访问"Xiaoming"。一个slash(/)表示某个特定命名空间的关键字,::xxx表示当前命名空间的关键字,::alizs/kw表示alizs命名空间下的kw关键字
  • 符号(symbol)也是标志符,但和关键字不同的是它在Clojure运行时被解析成它所对应的值,符号不能以数字开头,但可以包含一些*、+、!、-、_以及?这之类的特殊符号,同关键字一样包含一个/的符号表示在某个特定命名空间下的符号
  • Clojure提供了很完备的数字表示法

42,0xff, 2r111, 040     long (64-bit signed integer)
3.14, 6.0221415e23     double (64-bit IEEE floating point decimal)
42N                                  clojure.lang.BigInt (arbitrary-precision integer)
0.01M                              java.math.BigDecimal (arbitrary-precision signed floating point decimal)
22/7                                 clojure.lang.Ratio

  • 正则表达式在字符串前面加hash character(#)
  • 三种注释:;,#_()和 (comment ),;用作单行注释,comment宏会返回nil,#_()彻底注释
  • comma(')的作用完全可以通过whitespace( )代替,只有在为了增强可读性的时候才用
6. Clojure里提供的4个最基本的容器:
    '(a b :name 12.5) ;; list
    ['a 'b :name 12.5] ;; vector
    {:name "Chas" :age 31} ;; map
    #{1 2 3} ;; set
    其中list最前面的'是为了防止被认为是函数调用
7. 特殊语法糖:
  • 抑制计算用quote(')
  • 匿名函数可以用#()定义
  • 运算本身是被应用于被var所持有的value上,以#‘开头可以用来表示var本身
  • 引用类型的实例可以通过@前缀来解除引用
  • 宏有三种特殊语法表示:`,~和~@
  • 技术上来讲只有两种同Java交互的方式,但还是有一些其他的语法糖扩展
  • Clojure中所有的数据结构和引用类型都支持metadata,这些metadata不会对一些类似是否相等的运算产生影响,它们可以被应用于像是指明函数为namespace-private或指明函数返回值的类型等。可以通过^来添加metadata

8. namespace是Clojure的基本代码模块单元,所有的代码都是在某个namespace下定义和求值的。Clojure REPL session启动时总是以user为默认的namespace。当前的namespace可以通过*ns* 查看。事实上Clojure的namespace和Java的package是完全等价的。每个namespace下面[]/引入的(Java包都不同,默认会导入java.lang包,clojure.core里面的东西也会默认被分导入,两者类似。

9.(defn average [numbes] (/ (apply + numbers) (count numbers)))

10. 除去和Java的交互,放到函数位置可以被evaluate的只有两样:var或局部变量的值以及Clojure的special form。Special form是Clojure的原语,Clojure用它们来自己完善自己,这些原语很基本很基本,连when或者加、取反这样的操作都不是

11.常用原语:

  • (quote)(')抑制计算,就是当成符号本身,而不是取其值,例如:'(+ x x)相当与(list '+ 'x 'x),此外还可以用它窥探某个form的类型,像是''x
  • (do)用来对它包含的所有表达式进行求值运算,并以最后一个表达式的值作为其结果值,也就是除了最后一个表达式的值其他都不会被保留(都会被执行),很多form都在内部使用(do)
  • (def)用来再当前的namespace定义或重新定义一个var,还有很多以def开头的form在内部使用了def像是:defn、defn-、defprotocol、defonce、defmacro等,但是注意,并不是带def的form都可以用来定义或重新定义var,像是deftype、defrecord、defmethod等
  • (let)用来定义局部的引用,尤其在跟函数创建或函数定义有关的form中使用广泛,这些form都要用它来绑定函数所需的局部参数。(let)内部执行的某些运算是不许要将其赋值的,比如(println)会返回一个nil,这时候我们通常在绑定名字处用一个underscore(_)占位。用(let)赋值的局部变量(local)有两个特点:1、所有局部变量都是不可变的(immutable),但loop和recur两个special form为我们提供了每次循环所需的变量改变,此外Clojure当然也为我们提供了很多支持可变变量的引用类型。2、let的绑定向量是在编译时翻译的,这给我们提供了可选的对于普通collection类型的反构造(destructuring),反构造可以在很大程度上帮助我们去除一些特定的累赘代码,而这些代码通常和使用那些作为函数参数的传入的collection有关(不明白?接着往下看)
  • sequential和map作为Clojure中两种主要的抽象结构,被很多函数用作输入或输出值,请注意他们是抽象的,因此函数之间的耦合度可以降得很低,但给访问其中的值带来了一定麻烦。例如:(def v [42 "foo" 99.2 [5 2]]),可以用(first v)(second v)(last v)(nth v 2)(v 2)(.get v 2)等方式来访问其“top-level”的值(注:Clojure中所有的sequential collection都实现了java.util.List接口),但要访问[5 2]里面的2时就需要类似(last (last v)),第n层就要套n个括号。let允许把"top-level"中的一部分值取出来,实现destructuring,比如(let [[x y z] v] (+ x z))就会出来结果141.2,注意在这里我们用了一个向量作为名字而不是一个简单的标量符号名,这将使得v被sequentially destructured,赋值顺序也很好判断,从头排队。此外还可以这么玩(let [[x _ _ [y z] v]] v (+ x y z)),最后得到结果59,虽然像这种destructuring的嵌套层次没有限制,但最好别太多了,否则你懂的……还有两个tip:1、(let [[x & r] v])表示把第一个之后的所有值赋给r;2、(let [[x _ z :as original-vector] v])可以把original-vector绑定为修改之前的v,这样在后面如果想用之前的v就很方便了(在v是调用函数的时候才有用,否则直接用v不就完了)
  • map的绑定类似,它适用于Clojure的hash-maps、array-maps、record、实现了java.util.Map的collection以及所有支持get函数的结构(用index作为key)。以(def m {:a 5 :b 6 :c [7 8 9] :d {:e 10 :f 11} "foo" 88 42 false})为例,(let [{x :a y :b} m])相当于把5赋给x,把6赋给y;destructuring还适用于m里面非keyword的key,(let [{f "foo"} m]);刚刚说了(支持get),(let [{x 3 y 8} [a b c d e f g h i]])也是可以的。如果是多层次(let [{{ee :e} :d} m])会把10赋给ee,与sequential绑定结合(let [{[x _ y] :c} m]),总之很灵活,只要你想得到就能办得到。map除了支持:as提供原map的一个保留,还支持用:or提供一个默认备选的map,例如(let [{k :unknown x :a :or {k 50}} m])当k在m中找不到:unknown时就会以50作为其默认值,还支持用:keys、:strs、:syms把map当中所有以keyword、string、symbol为key的值都绑定过来,例如:(def chas {'name "Chas" 'age 31})之后就可以用(let [{:syms [name age]} chas])来进行对应的绑定。利用&可以直接把sequential结构的剩下的部分看作一个map,例如(def x [1 2 3 4 5 6])然后(let [[a b & {c 4 d 5}] x] (+ c d))结果为10
  • 函数是Clojure的一等公民,可以用fn这个special form来创建,来个简单例子(fn [x] (+ 10 x)),fn接受一个let风格的绑定向量(凡是let可以做的,接受参数这里都可以做,包括destructuring),fn的body部分其实是放在一个隐藏的(do)下面的,因此包含的每个form都会执行并以最后一个的结果作为整个函数的结果。函数可以支持多个参数,另外我们一般用defn宏来替代def+fn,例如:(defn strange-adder ([x] (strange-adder x 1))([x y](+ x y))),看到了吧,函数自己调用自己就是这么方便(有点类似Java里面的构造函数)。有时候我们需要函数相互调用,这时候用letfn这个special form例如:(letfn [(odd? [n] (even? (dec n))) (even? [n] (or (zero? n) (odd? (dec n))))] (odd? 11)),结果为true,这样来判断奇偶数是不是很恶心……想让参数数量更灵活吗?想让一些参数有默认值吗?想让参数顺序随便排列吗?试试基于map destructuring的keyword argument吧,例子:(defn make-user [username & {:keys [email join-date] :or {join-date (java.util.Date.)}}] {:username username :join-date join-date :email email}),函数接受一个必选参数username,然后把剩下的参数列表当成一组key-value对并从中选择以keyword:email和join-date为key的值,哦,join-date还有个默认值
  • Clojure中定义匿名函数(function literal / anonymous function)很容易,例如:#(Math/pow %1 %2)等价于(fn [x y] (Math/pow x y)),但有几点区别:1、(fn)定义的函数body被包在(do)里面,因此如果有多个表达式都会被依次运算,但用#()定义的函数需要手工完成这一步。2、注意#()里面的参数直接用它的次序来表示,像是%1、%4,可以用%来表示第一个参数,也可以用%&表示剩下参数所组成的vector,还有这种function literal不允许嵌套,否则根本没法识别参数
  • if作为Clojure仅有的条件原语,很是简单。(if a b c),如果a的结果是nil或者false那么就c,否则b(注意:这与用来判断是否为true或false的true?和false?不一样),其中a b是必须要有的,如果没有c则需要用到时会得到nil。刚刚说了if是条件原语的独苗,很多东西都用到它,比如:when、cond、if-let以及when-let
  • Clojure为我们提供了很多有用的循环结构,包括deseq和dotimes,所有这些都是基于递归(recur)来实现的,它可以在不消耗栈空间的前提下将控制转交给最近的一个loop开始处(不一定是loop也可能是别的函数),上例子:(loop [x 5] (if (neg? x) x (recur (dec x)))),loop初始值的绑定也是通过let完成的,然后当x还是非负时recur就会将控制转移到loop开始处,并将recur中每一个表达式的结果,按照顺序重新赋给开始绑定的局部变量(因此开始有多少变量recur内就要有几个对应的表达式)。如果开始为多个局部变量赋值,例如x和y,绑定顺序是x在前y在后,但recur有关y的表达式在前有关x的表达式在后,那么就会将和y有关的表达式结果给x,同理x的给y,有点罗嗦……刚刚说了,recur还可以作用于函数,比如(defn countdown [x] (if (zero? x) :blastoff! (do (println x) (recur (dec x)))))。虽然我们学了recur,但是由于它太底层了,大多数时间我们都会选用更高级一点的循环form,像是控制流程的doseq和dotimes,遍历collection的map、reduce和for等等。由于recur不消耗栈空间(不会出现stack overflow),因此在实现一些特定的递归算法的时候还是会用到它,此外在某些数值相关的地方使用它还可以避免【boxed representation】问题,最后对于一些加加减减的复杂集合,当map、reduce做不了或太低效的时候,果断大胆的用起recur
  • (def x 5)当需要引用x本身而非其值5的时候,可以用(var x)或#'x
  • 所有同Java的交互都用到new和.这两个special form,虽然它们一般不会出现,是因为Clojure提供了一些更加便捷的语法糖。

new java.util.ArrayList(100) == (new java.util.ArrayList 100) == (java.util.ArrayList. 100)(注意那个.)

Math.pow(2, 10) == (. Math pow 2 10) == (Math/pow 2 10)

"hello".substring(1, 3) == (. "hello" substring 1 3) == (.substring "hello" 1 3)

Integer.MAX_VALUE == (. Integer MAX_VALUE) == Integer/MAX_VALUE

someObject.someField == (. someObject someField) == (.someField someObject)


 

  • 异常交由try和throw两个special form处理
  • 虽然Clojure一直强调不可变的数据结构和值,但也提供了set!来改变一些线程内变量或Java字段以及由deftype定义的可变域的值
  • Clojure提供了一些用作同步控制的原语,但通常不必使用他们,locking宏可以帮助完成一切
  • 实际上我们完成的所有的运算都是放在(eval)里面的,基本上不需要显示使用它,通常总能找到替代的宏来更好的完成任务

好了,接下来可以正式开始学习了……


你可能感兴趣的:(Clojure,Programming)