ivaneye.com

Clojure教程-函数

本文翻译自Functions in Clojure

本文包括如下内容:

版权:

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