Clojure 确实要比 Python 语义上庞大, 所以无法尽量在一篇之中收纳进来, 只得另立新篇, 也许还会有第三篇笔记. 现在开始学习函数定义
函数定义
函数用 defn
宏来定义, 函数名与参数列表之间可选的字符串是函数的注释, 相当于 Python 的函数体中第一个字符串, 这个字符串能被 (doc func-name)
列出来, Python 中是用 dir(fun_name
显示函数帮助. 不需要 return 关键字, 和 Groovy/Scala 一样最后一个表达式的值为函数的返回值, 所以函数总是有返回值(或为 nil).
和 C 语言一样, 函数必须先定义再使用, 否则要用 (declare function-names)
提前声明, 下面代码是在 Clojure 的 REPL 中执行的
1 2 3 4 5 6 7 8 9 10 11 |
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 中, 如
1 2 3 4 |
(defn accumulate [first & others] (reduce #(+ %1 %2) first others)) (println (accumulate 1 2 3 4 5 6)) ;-> 21 |
函数可包含多个参数及对应的方法体, 以此来实现默认参数或重载
1 2 3 4 5 6 7 |
(defn sayHello ([] (sayHello "World")) ; 相当于定义了两个函数了, 瞧这里正调用第二个函数 ([name] (str "Hello " name)) ) (println (sayHello)) ; -> Hello World (println (sayHello "Yanbin")) ; -> Hello Yanbin |
匿名函数已在第一篇中讲过了, 它其实就是一个 Lambda 表达式了, #(...), 参数直接用 %(%1), %2, %3 .... 了. Clojure 的这种方式的匿名函数还是很方便的.
comp
可以把多个函数以管道形式组合起来, 如
1 2 3 4 5 6 7 8 9 |
(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 定义谓词型的函数使用问号, 像
1 2 3 |
(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
正确的定义出来.
1 2 3 |
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) 这样来查看使用方法. 大概用代码罗列一下:
1 2 3 4 5 6 7 8 9 10 |
(import ;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),相当于把 . 当作函数名,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
也能链接:
1 2 3 4 |
(.. 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 的条件语句是有返回值的. 下面一起进到代码里去理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
(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
1 2 3 4 |
(dotimes [card-number 3] ;card-number 是一个本地 binding, 如果用不上就写成 (dotimes [_ 3]), 和 Scala 一样 (print card-number)) ;-> 012 (while true (print ".")) |
宏 for
, doseq
和 special form loop
, 它们可以用 :when
或 :while
进行过滤, 例子:
1 2 3 4 5 6 7 8 9 10 11 |
(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" 这本书要细细的品读了.
本文链接 https://yanbin.blog/clojure-get-started-1/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。