Clojure程序设计
上QQ阅读APP看书,第一时间看更新

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?需要在多处调用时,这是理所当然的明智之选。

匿名函数只是一种选择,而非必须。只有当你发现它们可以令你的代码更具可读性时,才应该使用这样的匿名形式。如果开始越来越频繁地使用它们,不必惊讶,这只不过是你开始有一点习惯它们了。