Clojure 快速突击(续)

Clojure 确实要比 Python 语义上庞大, 所以无法尽量在一篇之中收纳进来, 只得另立新篇, 也许还会有第三篇笔记. 现在开始学习函数定义

函数定义

函数用  defn 宏来定义, 函数名与参数列表之间可选的字符串是函数的注释, 相当于 Python 的函数体中第一个字符串,  这个字符串能被 (doc func-name)  列出来, Python 中是用 dir(fun_name 显示函数帮助. 不需要 return 关键字, 和 Groovy/Scala 一样最后一个表达式的值为函数的返回值, 所以函数总是有返回值(或为 nil).

和 C 语言一样, 函数必须先定义再使用, 否则要用 (declare function-names) 提前声明, 下面代码是在 Clojure 的 REPL 中执行的

user=> (defn say-hello-to
 "say hello to some"
 [name] (str "Hello, " name))
#'user/say-hello-to
user=> (doc say-hello-to)
-------------------------
user/say-hello-to
([name])
 say hello to some
nil
user=>

提一下 Clojure 的命名规则, 变量和函数名用中划线连接的小写单词. defn- 定义的函数是私有, 只对当前名字空间可见, 比如上面的  user 名字空间. 有点像 Python 的下划线变量或函数的可见性约定.

Clojure 支持可变函数(VarArgs), & 之后的参数在一个 list 中, 如

(defn accumulate [first & others]
 (reduce #(+ %1 %2) first others))

(println (accumulate 1 2 3 4 5 6))   ;-> 21

函数可包含多个参数及对应的方法体, 以此来实现默认参数或重载

(defn sayHello
 ([] (sayHello "World"))       ; 相当于定义了两个函数了, 瞧这里正调用第二个函数
 ([name] (str "Hello " name))
 )

(println (sayHello))          ; -> Hello World
(println (sayHello "Yanbin")) ; -> Hello Yanbin

匿名函数已在第一篇中讲过了, 它其实就是一个 Lambda 表达式了, #(...), 参数直接用 %(%1), %2, %3 .... 了. Clojure 的这种方式的匿名函数还是很方便的.

comp 可以把多个函数以管道形式组合起来, 如

(defn times2 [n] (* n 2))
(defn minus3 [n] (- n 3))

(def my-composition (comp minus3 times2))  ;注释 用的 def, my-composition 是一个函数类型的变量

(println (my-composition 4))  ;-> 4 * 2 -3 = 5

(defn my-composition1 [n] (-> (times2 n) minus3))
(println (my-composition1 4)) ; 同样的效果 4 * 2 - 3 = 5 

complement 接受函数为参数求补, partial 给旧的函数制定一个初始值. 

与 Ruby  的约定一样, Clojure 定义谓词型的函数使用问号, 像

(defn teenager? [age] (and (>= age 13) (< age 20)))
(teenager? 15)   ;-> true
(teenager? 20)   ;-> false

学习 Clojure 到现在基本能够体会到它的代码就是数据, 代码中常用的 () 和 [] 怎么变都是 List 和 Vector 的组合, 再多就是把 List 和 Map 加进去. 数据结构就是编程, 谁想到 Lisp 这么一种语言真是太有才了.

发现一个有意思的事情, 下面的代码因为输入有误, 结果是 Cloujure 把结果都算出来了, 再告诉我最后多了一个括号, 可见它的执行机制是能匹配到括号就能行, 多了也不是大事. 可以试下 (def v 123))))))) 再多的括号都能为你把变量 v = 123 正确的定义出来.

user=> (reduce #(+ %1 %2) [1 2 3 4 5]))
15
RuntimeException Unmatched delimiter: ) clojure.lang.Util.runtimeException (Util.java:221)

Java 互操作

Clojure 使用 Java 的东西可没有 Scala 那么简单, 主要了解几个宏 import, ., .., .?., 以及 doto 函数. Clojure 为同一个操作准备了多种方式, 看似灵活, 其实让人更凌乱. 使用 (doc import), (doc doto) 这样来查看使用方法. 大概用代码罗列一下:

(import 
  '(java.util Calendar GregorianCalendar)
  '(javax.swing JFrame JLabel))

Calendar/APRIL  ;->3 如果 Calendar 已导入, 或 (. java.util.Calendar APRIL)  访问类常量
(Math/pow 2 4)  ; 或 (. Math pow 2 4)

(def calendar (GregorianCalendar. 2008 Calendar/APRIL 16)) ;或 ..(new GregorianCalendar 2008 3 16)
(. calendar add Calendar/MONTH 2)
(.get calendar Calendar/MONTH)  ; 两种方式调用实例方法

真正要使用的时候再回过头来看吧, 主要有两点比较新颖: 1) .. 能把方法串接起来 2) doto 可用来调用一个对象的多个方法, 方法不要求 return this 也能链接:

(.. calendar getTimeZone getDisplayName) ;相当于 (. (. calendar getTimeZone) getDisplayName), .?. 短路操作,可容错
(doto calendar                  ; doto 在每次调用下面的方法都会返回它的第一个参数 calendar
  (.set Calendar/YEAR 1981)
  (.set Calendar/MONTH Calendar/AUGUST))

所有的  Clojure 方法都实现了 java.lang.Runnable 接口. Clojure 的异常都是运行时异常. 如果要捕获从 Java 代码抛出的检测异常需要用到 try, catch, finally, 和  throw 那些被称作 special forms 的东西.

流程控制

条件处理, special form if, 宏 when, when-not, 能绑定 binding 变量的宏 if-let 和 when-let, 还有相当于 switch/case 语句的 condp 和 cond 两个宏.do 可以用来包裹多个操作, 方便在单个条件中执行多条语句. 和 Scala 一样, Clojure 的条件语句是有返回值的. 下面一起进到代码里去理解

(if true                   ;if 可有三个参数, 第一个为条件, 第二个为成立时表达式, 第三个为可选的不成立时的表达式
  (println "play")
  (do (println "work")     ; 有三个条件时, 相当于 ? : 三无操作符
    (println "sleep")

(when is-weekend (println "play"))  ;只在条件成立时干什么
(when-not is-weekend (println "work") (println "sleep")

(if-let [name (first waiting-line)]  ;如果 waiting-line 为空时, first 返回 nil 就代表 false
  (println name "is next")
  (println "no waiting"))
(when-let [head (first coll)] (print head)

(condp = value  ;第一个参数谓词  =, 第二个参数要比较的值 value, 后面任意多个值-表达式对, 最后为 default
  1 "one"
  2 "two"
  (str "unexpected value, " value)) ;这个是 default

(cond   ;与 condp 不同的是, 它可以提供不同的比较方式
  (instance? String temperature) :invalide temperature"
  (<= temperature 0) "freezing"
  (>= temperature 100) "boiling"
  true "neither"))

condp 和 cond 匹配不到条件就会抛出 IllegalArgumentException, 每个分支自带 break 功能.

循环, 用宏 dotimes 和 while

(dotimes [card-number 3]  ;card-number 是一个本地 binding, 如果用不上就写成 (dotimes [_ 3]), 和 Scala 一样
  (print card-number))  ;-> 012

(while true (print "."))

for, doseq 和 special form loop, 它们可以用 :when 或 :while 进行过滤, 例子:

(def cols "ABCD")
(def rows (range 1 4))

(dorun  ;dorun 不让 for 循环返回集合, 没有 dorun 的话 for 有返回值 (nil nil nil nil nil nil)
  (for [col cols :when (not= col \B)  ;\B 语法糖表示字符 B
    row rows :while (< row 3)]   ;多重循环只要写在这个 Vector 中就行,单循环为 (for [col cols] (prinltn col))
      (println (str col row))))

(doseq [col cols :when (not= col \B)
  row rows :while (< row 3)]
    (println (str col row)))

Clojure 的循环与 Java 的  foreach 写法有点类似.

往下是递归, Clojure 和 Java 一样也是不支持尾递归优化的, loop/recur 组合可把一个看似递归的调用变成一个迭代, 这是做了尾递归优化该做的事情了.

已经不想再往下写了, 先了解这些 Clojure 的基础知识了, 剩下的还有一本 "Clojure IN ACTION" 这本书要细细的品读了.

 

类别: Clojure. 标签: . 阅读(27). 订阅评论. TrackBack.

Leave a Reply

Be the First to Comment!

avatar
wpDiscuz