Clojure教程-函数
本文翻译自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作为参数.
关键字的默认值依据:or后跟的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