本文翻译自Functions in Clojure
本文包括如下内容:
- 如何定义函数
- 如何执行函数
- 多元数函数(Multi-arity Functions)
- 不定参函数(Variadic Functions)
- 高阶函数
- 其它函数相关内容
版权:
This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available on Github.
目录
针对Clojure版本
Clojure 1.5
简介
Clojure是函数式编程语言.自然的,函数是Clojure非常重要的一部分.
如何定义函数
函数定义一般使用defn宏:
(defn round [d precision] (let [factor (Math/pow 10 precision)] (/ (Math/floor (* d factor)) factor)))
类型提示有时能避免编译器使用反射,从而能生成更高效的字节码.但是,基本上
你没必要使用类型提示.后期优化时再考虑.
函数可以添加注释文档,给API添加文档说明是个好习惯:
(defn round "Round down a double to the given precision (number of significant digits)" [d precision] (let [factor (Math/pow 10 precision)] (/ (Math/floor (* d factor)) factor)))
在Clojure中函数参数可以有类型提示,不过是可选的.
(defn round [^double d ^long precision] (let [factor (Math/pow 10 precision)] (/ (Math/floor (* d factor)) factor)))
函数还可以定义前置和后置条件来限制函数的参数和返回值.
(defn round "Round down a double to the given precision (number of significant digits)" [^double d ^long precision] {:pre [(not-nil? d) (not-nil? precision)]} (let [factor (Math/pow 10 precision)] (/ (Math/floor (* d factor)) factor)))
在上面的例子中,我没使用了前置条件来检查两个参数是否为nil.not-nil?宏(或
函数),没有在该例子中展示,我们假设它已经在其它地方实现了.
匿名函数
匿名函数使用fn特殊形式来定义;
(fn [x] (* 2 x))
匿名函数可以赋给局部变量,作为参数传递给函数或作为函数的返回值.
(let [f (fn [x] (* 2 x))] (map f (range 0 10)))
Clojure提供了语法糖来简化匿名函数的编写:
(let [f #(* 2 %)] (map f (range 0 10)))
%表示第一个参数.如果要引用多个参数,可以使用%1,%2.以此类推:
;; 一个包含了三个参数的匿名函数,返回三个参数的和 (let [f #(+ %1 %2 %3)] (f 1 2 3))
语法糖简化了代码,但是降低了代码的可读性.所以使用前请斟酌.
如何执行函数
要执行函数,只需要将函数名放在list的第一个位置就行了:
(format "Hello, %s" "world")
对于赋给局部变量,变量或这从参数传递的函数,此法同样适用:
(let [f format] (f "Hello, %s" "world"))
另外你也可以使用clojure.core/apply来执行函数
(apply format "Hello, %s" ["world"]) (apply format "Hello, %s %s" ["Clojure" "world"])
clojure.core/apply一般在调用不定参函数或者需要将参数作为集合传递时才会
使用
多元数函数
在Clojure中有多元数函数:
(defn tax-amount ([amount] (tax-amount amount 35)) ([amount rate] (Math/round (double (* amount (/ rate 100))))))
在上面的例子中,只有一个参数的函数调用了有两个参数的函数.这在多参函数中
很常见(相当于默认值的功能).Clojure没有提供默认值的功能,是因为JVM不支持.
在Clojure中,元数只和参数个数有关,而和参数类型无关.这是因为Clojure是动
态语言,类型信息可能在编译期是无效的.
(defn range ([] (range 0 Double/POSITIVE_INFINITY 1)) ([end] (range 0 end 1)) ([start end] (range start end 1)) ([start end step] (comment Omitted for clarity)))
解构函数参数
有时函数的参数是数据结构:向量,序列,map.当你想访问这些数据结构的其中一
部分数据时,你可能需要编写类似下面的代码
(defn currency-of [m] (let [currency (get m :currency)] currency))
对向量来说,需要编写类似这样的代码:
(defn currency-of [pair] (let [amount (first pair) currency (second pair)] currency))
但是呢,这样的样板代码可重用性并不高.所以Clojure提供了解构.
基于位置的解构
解构向量的方式如下:使用一个向量替换原来作为函数参数的数据结构,这个向量包含了占位符,
而占位符会将对应位置的数据结构的值绑定过来.举例来说,如果一个参数是一对
值,你想要获得第二个参数值,那么代码可以这样写:
(defn currency-of [[amount currency]] currency)
在上面的例子中,参数的第一个值被绑定到了amount上,第二个参数是被绑定到了
currency上.看起来很棒,但是,这里我们并没有使用amount.在这种情况下,我们
可以使用下划线来忽略它:
(defn currency-of [[_ currency]] currency)
解构是能够嵌套的:
(defn first-first [[[i _] _]] i)
虽然本文不会涉及到let这个form和本地变量.但是需要提一下,解构对let也生效,而
且作用一模一样
(let [pair [10 :gbp] [_ currency] pair] currency)
解构Map
对Map和Record的解构方式与解构向量略有不同:
(defn currency-of [{currency :currency}] currency)
在上面的例子中,我们想把:currency这个key对应的value绑定到currency上.Key
并不一定需要是关键字:
(defn currency-of [{currency "currency"}] currency) (defn currency-of [{currency 'currency}] currency)
我们可以一次性解构多个key:
(defn currency-of [{:keys [currency amount]}] currency)
上面的例子中,keys需要为关键字,其名字与currency和amount相同
(即:currency,:amount).如果keys是字符串,则将上面的:keys改为:strs即可:
(defn currency-of [{:strs [currency amount]}] currency)
当然也可以是symbol:
(defn currency-of [{:syms [currency amount]}] currency)
当然了,使用关键字作为key在Clojure中是推荐做法.
解构Map时,如果找不到我们需要的key的值,我们可以设置默认值:
(defn currency-of [{:keys [currency amount] :or {currency :gbp}}] currency)
此功能对于编写包含额外属性的函数大有裨益.
和基于位置的解构相同,Map解构对let同样适用:
(let [money {:currency :gbp :amount 10} {currency :currency} money] currency)
变参函数
参数数量可变的函数叫做变参函数.clojure.core/str和clojure.core/format就
是两个变参函数:
(str "a" "b") ; ⇒ "ab" (str "a" "b" "c") ; ⇒ "abc" (format "Hello, %s" "world") ; ⇒ "Hello, world" (format "Hello, %s %s" "Clojure" "world") ; ⇒ "Hello, Clojure world"
要定义变参函数,只需要在变参前面加个&就可以了:
(defn log [message & args] (comment ...))
在上面的例子中,只有一个参数是必须的.变参函数的调用方式和普通函数相同:
(defn log [message & args] (println "args: " args)) (log "message from " "192.0.0.76")
在REPL中执行:
user=> (log "message from " "192.0.0.76") args: (192.0.0.76) user=> (log "message from " "192.0.0.76" "service:xyz") args: (192.0.0.76 service:xyz)
你可以看到,可选的参数被包装到了一个list里面.
具名参数(Named Parameters)
具名参数是通过对变参函数的解构来实现的.
从解构变参函数的立场上来看,具名参数具有较好的可读性.下面是一个例子:
(defn job-info [& {:keys [name job income] :or {job "unemployed" income "$0.00"}}] (if name [name job income] (println "No name specified")))
使用函数方式如下:
user=> (job-info :name "Robert" :job "Engineer") ["Robert" "Engineer" "$0.00"] user=> (job-info :job "Engineer") No name specified
如果不使用变参列表,那么你需要使用形如{:name “Robert” :job “Engineer”}这
样的map作为参数.
关键字的默认值依据:as后跟的map来确定.如果关键字没有传递值,且无默认值,
则为nil.
高阶函数
高阶函数是将其它函数作为参数的函数.高阶函数在函数式编程中是很重要的技
术,在Clojure中经常使用到.一个高阶函数的例子是将一个函数和一个集合作为
参数,返回符合这个函数条件的集合.在Clojure中,这叫做clojure.core/filter:
(filter even? (range 0 10)) ;=>(0 2 4 6 8)
上面的例子中,clojure.core/filter函数接收clojure.core/even?作为参数.
clojure.core中有很多高阶函数.经常使用的函数请参见clojure.core
私有函数
在Clojure中,函数可以在其命名空间中设置为私有的.
具体细节请参考这里
关键字作为函数
在Clojure中,关键字可以作为函数使用.他们接收map或record并从中查找信息:
(:age {:age 27 :name "Michael"}) ; ⇒ 27
他们经常和高阶函数结合使用:
(map :age [{:age 45 :name "Joe"} {:age 42 :name "Jill"} {:age 17 :name "Matt"}]) ; ⇒ (45 42 17)
也能和->宏一起使用:
(-> [{:age 45 :name "Joe"} {:age 42 :name "Jill"}] first :name) ; ⇒ "Joe"
Map作为函数
Clojure的Map也能作为函数使用,来查找key对应的value:
({:age 42 :name "Joe"} :name) ; ⇒ "Joe" ({:age 42 :name "Joe"} :age) ; ⇒ 42 ({:age 42 :name "Joe"} :unknown) ; ⇒ nil
需要注意的是,虽然大部分情况下map和record可以等同对待,但是这里不
行!record不能作为函数使用!
Set作为函数
(#{1 2 3} 1) ; ⇒ 1 (#{1 2 3} 10) ; ⇒ nil (#{:us :au :ru :uk} :uk) ; ⇒ :uk (#{:us :au :ru :uk} :cn) ; ⇒ nil
此功能被用来验证某值是否set中:
(when (countries :in) (comment ...)) (if (countries :in) (comment Implement positive case) (comment Implement negative case))
因为在Clojure中除了false和nil,其它值都是true.
函数作为比较器
Clojure函数实现了java.util.Comparator接口,所以能作为比较器使用.
结束语
函数是Clojure的核心.他们通过defn宏来定义,可以有多个元数,不定参数并支持
参数解构.函数参数和返回值可以有类型提示,当然这不是必须的.
函数是一等值,它能被传递给其它函数.这是函数式编程的基石.
有一些数据类型有函数特性.适时的使用这些特性可以是代码更简洁易读.
贡献
Michael Klishin michael@defprotocol.org, 2012 (original author)
Translated by Ivan 2014