本文包括如下内容:
- 如何定义函数
- 如何执行函数
- 多元数函数(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是函数式编程语言.自然的,函数是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)
(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")
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