2.3 函数
所谓函数调用,在Clojure中,只不过是一个列表的起始元素可以被解析成函数而已。例如,下面调用了str函数,并将它的参数连接为一个字符串。
(str "hello" " " "world") -> "hello world"
函数名通常表明了其单复数,例如clear-agent-errors。如果函数是一个谓词,那么按照惯例,它的名称应该以一个问号结束。如下所示,这些以问好结尾的谓词,会对它们的参数进行类型测试。
(string? "hello") -> true (keyword? :hello) -> true (symbol? 'hello) -> true
可以使用defn来定义你自己的函数。
(defn name doc-string? attr-map? [params*] body)
attr-map表示关联到函数对象上的元数据。详情请参阅第2.8节“元数据”。为了说明函数定义中的其他部分,下面创建一个 greeting 函数,它接受一个名称,然后返回以“Hello”开头的问候语。
src/examples/exploring.clj (defn greeting "Returns a greeting of the form 'Hello, username.'" [username] (str "Hello, " username))
你可以这样来调用greeting。
(greeting "world") -> "Hello, world"
你也可以这样来查阅greeting的文档。
user=> (doc greeting)
-------------------------
exploring/greeting
([username])
Returns a greeting of the form‘Hello, username.’
如果greeting的调用者遗漏了username参数会如何?
(greeting) -> ArityException Wrong number of args (0) passed to: user$greeting clojure.lang.AFn.throwArity (AFn.java:437)
Clojure 函数强调元数(arity),也就是它们期望获得的参数数量。如果你调用函数时传入的参数数目不正确,Clojure 会抛出一个 ArityException。如果你希望当调用者遗漏了username参数时,greeting函数也能表达通用的问候,那么你可以使用defn的另外一种形式,它允许函数接受多组参数列表和函数主体。
(defn name doc-string? attr-map?
([params*] body)+)
同一个函数的不同元数之间能够彼此相互调用,所以,你就可以很容易地创建一个没有参数的greeting,然后将功能委托给那个单参数的greeting,并传入一个默认的username。
src/examples/exploring.clj (defn greeting "Returns a greeting of the form‘Hello, username.’ Default username is‘world’." ([] (greeting "world")) ([username] (str "Hello, " username)))
检验一下这个新的greeting是否符合预期。
(greeting) -> "Hello, world"
在参数列表中包含一个&号,你就能创建一个具有可变元数的函数。Clojure会把所有剩余的参数都放进一个序列中,并绑定到&号后面的那个名称上。
下面的函数允许两个人约会时有可变数量的监护人相随。
src/examples/exploring.clj (defn date [person-1 person-2 & chaperones] (println person-1 "and" person-2 "went out with" (count chaperones) "chaperones.")) (date "Romeo" "Juliet" "Friar Lawrence" "Nurse") | Romeo and Juliet went out with 2 chaperones.
在递归定义中,变参非常有用。具体示例请参阅第4章“函数式编程”。
为不同的元数编写函数实现是非常有用的。但如果你有面向对象编程的背景,那么你一定还会联想到“多态”(polymorphism),也就是能够根据类型的不同,来选取相应的实现。Clojure能够做到的远不止于此。请参阅第8章“多重方法”和第6章“协议和数据类型”以获取更多详情。
defn意在命名空间的顶层定义函数。但如果你希望能够通过函数来创建函数,那就应该使用匿名函数加以替代。
2.3.1 匿名函数
除了能用 defn 来创建具名函数以外,你还能用 fn 创建匿名函数。采用匿名函数至少有以下3个原因。
● 这是一个很简短且不言自明的函数,如果给它取名字的话,不会令可读性增强,反而使得代码更难以阅读。
● 这是一个仅在别的函数内部使用的函数,需要的是局部名称,而非顶级绑定。
● 这个函数是在别的函数中被创建的,其目的是为了隐藏某些数据。
用作过滤器的函数总是简短且不言自明的。例如,假设你要为一个由单词组成的序列创建索引,同时你并不关心那些字符数小于3的单词。那么,你可以编写一个这样的indexable-word?函数。
src/examples/exploring.clj (defn indexable-word? [word] (> (count word) 2))
接下来,你可以使用indexable-word?从句子中提取那些可索引的单词。
(require '[clojure.string :as str]) (filter indexable-word? (str/split "A fine day it is" #"\W+")) -> ("fine" "day")
上例中通过调用split,将句子分解为单词,然后filter对每个单词调用indexable-word?,并返回那些被indexable-word?判定为true的单词。
匿名函数能让你仅用一行代码就做到相同的事情。下面是最简单的匿名函数形式fn。
(fn [params*] body)
通过这种形式,你能在调用filter时直接插入indexable-word?的实现。
(filter (fn [w] (> (count w) 2)) (str/split "A fine day" #"\W+")) -> ("fine" "day")
还有一种采用隐式参数名称,也更加简短的匿名函数语法。其参数被命名为%1、%2,以此类推。对于第一个参数,你也可以使用%表示。该语法形如下。
#(body)
你可以使用这种更简短的匿名形式来重新调用filter。
(filter #(> (count %) 2) (str/split "A fine day it is" #"\W+")) -> ("fine" "day")
使用匿名函数的第二个动机是,确定想要一个具名函数,但该函数仅在其他函数的作用域内使用。继续这个indexable-word?的例子,你可以像下面这样写。
src/examples/exploring.clj (defn indexable-words [text] (let [indexable-word? (fn [w] (> (count w) 2))] (filter indexable-word? (str/split text #"\W+"))))
let将你刚才写的那个匿名函数与名称indexable-word?绑定在了一起,但这次它被限定在了indexable-words的词法作用域内。关于let的更多细节,请参见第2.4节“变量、绑定和命名空间”。验证一下这个indexable-words,看其是否符合预期。
(indexable-words "a fine day it is") -> ("fine" "day")
采用这种let和匿名函数的组合,相当于你对代码的读者说:“函数indexable-word?有足够的理由拥有一个名称,但仅限于在indexable-words中。”
使用匿名函数的第三个原因是,有时你需要在运行期动态创建一个函数。此前,你已经实现了一个简单的问候函数greeting。拓展一下思路,你还可以创建一个用来创建greeting函数的make-greeter函数。make-greeter函数接受一个greeting-prefix参数,并返回一个新函数,这个新函数会将greeting-prefix和一个姓名组合起来,成为问候语。
src/examples/exploring.clj (defn make-greeter [greeting-prefix] (fn [username] (str greeting-prefix ", " username)))
一般来说,为一个通过 fn 得到的函数命名是没有意义的,因为每次调用make-greeter都会创建一个不同的函数。然而,在某次调用了make-greeter之后,你也许会想为那个特殊的具体结果命名。如果真是这样,你可以使用 def 对 make-greeter创建的函数进行命名。
(def hello-greeting (make-greeter "Hello")) -> #'user/hello-greeting (def aloha-greeting (make-greeter "Aloha")) -> #'user/aloha-greeting
现在,你就可以像调用其它函数那样调用这些函数了。
(hello-greeting "world") -> "Hello, world" (aloha-greeting "world") -> "Aloha, world"
此外,没有必要为每个问候函数都命名。你可以简单地创建一个问候函数,并将它放置到列表形式的第一个(函数)槽(slot)中。
((make-greeter "Howdy") "pardner") -> "Howdy, pardner"
如你所见,不同的问候函数会记住创建它们时的那个greeting-prefix值。用更为正式的说法,这些问候函数对greeting-prefix的值构成了闭包(closures)。
2.3.2 何时使用匿名函数
匿名函数那极度简洁的语法并不总是恰当的。也许你实际上更偏向于明确化,喜欢创建诸如 indexable-word?这样的命名函数。这完全没有问题,并且如果 indexable-word?需要在多处调用时,这是理所当然的明智之选。
匿名函数只是一种选择,而非必须。只有当你发现它们可以令你的代码更具可读性时,才应该使用这样的匿名形式。如果开始越来越频繁地使用它们,不必惊讶,这只不过是你开始有一点习惯它们了。