`
songry
  • 浏览: 82987 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

practical_clojure chapter3 控制程序流(未完)

 
阅读更多

函数

 

    作为函数式编程语言,函数是每个Clojure程序的开始和结束。Clojure的编程模型就象一棵树,每个函数又衍生出对其他数个函数的调用。

    理解Clojure程序其实就是理解程序中包含的函数以及调用关系。胡乱地使用函数会使你的Clojure程序极度纠结。深思熟虑地使用函数会使你的代码高效、优雅,真正便于读写。

 

 

一级函数

 

    在Clojure中,所有的函数都是一级的对象,因为:

  • 它们能够在程序执行过程中的任意点被动态创建。
  • 它们没有固定命名,能够被绑定在多个符号上面而不是一个。
  • 它们可以做为值被储存在任何数据结构中。
  • 它们可以做为其他函数的参数或者返回值。

    对比一下其他静态语言中的函数,比如c或者java 。在这些语言中,函数在编译前,就需要预先定义好并赋予函数名。而在Clojure(或者其他函数式编程语言)中,函数可以在运行中定义,而且可以储存在任意的数据结构中,这是一个极大的优势。

 

 

用fn来定义函数

 

    定义函数的最基本方式是采用特殊form fn,它执行后能产生一个新的一级函数。在最简单的情况下,fn接收两个参数:一个vector参数做为函数符号,一个表达式做为函数调用的主体。


注意:vectors,被方括号限定,还没提及过。针对它具体语法的解释,参见Chapter 4。现在,你可以认为它是list表达式的一种替换方式。不像list被圆括号限定,vector 不表示函数的调用,所以它更适合在代码中快速清晰地表达纯粹的数据结构。


    举个例子,在REPL里面定义一个简单的函数,它接收两个参数,返回两个参数相乘的结果:

user=> (fn [x y] (* x y))

    这个form看起来有一点费解,但其实非常简单。它包含了3个form:fn , [x y] 和 (* x y)。fn被调用,其他两个form做为fn的参数,vector 定义了一个新的函数包含两个参数,x和y,(* x y)是函数的主体部分,将参数x,y绑定到对应的参数上面。这儿没有一个明确的返回部分,函数总是返回主体中调用表达式的值。


    然而,这样定义并没什么大的作用。它最会返回一个函数,被REPL转换为字符串打印出来。函数的字符串表现并不特别的漂亮或者有用:

#<user$eval__43$fn__45 user$eval__43$fn__45@ac06d4>

    而且,执行完成之后你没法使用这个函数。因为你根本没把这个函数绑定到任何的符号或者放到任何的数据结构里面去。JVM或许会立刻将它回收掉,因为它没被任何东西使用。通常,我们会把它绑定到一个符号上面去,就像这样:

user=> (def my-mult (fn [x y] (* x y)))

    现在,你能够通过这个符号来使用函数了:

user=> (my-mult 3 4)
12


    然后,它如同我们预期的那样给出了结果。表达式(fn [x y] (* x y))被解析为一个一级函数,然后绑定在符号my-mult上。在list里面将my-mult做为第一个元素就可以调用刚刚我们定义的新函数,后面两个元素3和4则是这个函数的参数。


    注意,无论如何,将函数绑定在符号上面只是使用它的一种方式,只要在list的首位,不管是符号还是其他啥东西,都会被调用。比如,我们其实完全可以直接把函数定义放在同一个form里面:

user=> ((fn [x y] (* x y)) 3 4)
12

    注意整个函数定义(fn [x y] (* x y)) 作为了form的第一个元素,当form执行时,会调用这个函数并将3和4做为参数传给它,执行的结果跟把函数定义绑定在符号上再调用符号的结果是一样的。


    最重要的是要记住,函数并不等同于绑定它们的符号。在之前的例子中,my-mult并不是个函数,它只是绑定到函数的一个符号。当它被调用时,并不是调用了my-mult,而是通过my-mult获取到函数,然后依次调用函数。

 

 

使用defn定义函数

 

    尽管函数与它们绑定的符号不同,但将函数命名并绑定到一个特定的符号上以备使用是最常见的场景。为了达到这个目的,Clojure提供了defn做为快捷方式来定义函数并绑定到一个符号。defn在语义上等于同时使用def和fn,但是更简洁和方便。它更是为函数定义提供了备注,以说明函数的作用。


    defn form接收这些参数:一个符号名,一段文本字符串(可选的),一个参数vector,和一个函数主体表达式。比如,下面这段函数就定义了求一个数的平方:

user=> (defn sq
"Squares the provided argument"
[x]
(* x x))

    你可以使用指定的名字来调用这个函数:

user=> (sq 5)

25

    你也可采用内建的doc函数来查看任何函数,它会将函数的信息(包含说明文字)输出到控制台:

user=> (doc sq)
---------------------
user/sq
([x])
Squares the provided argument
nil

 

 

多重参数数量函数

 

    参数数量是指函数接收参数的个数,在Clojure中,我们能够基于不同的参数数量定义函数的多个实现版本。


    这同样是用我们之前讨论过的fn或者defn,但是在参数上有一点小小的差别。不像刚刚只采用一个单独的参数vector和函数体表达式那样,你能够写出多个参数/表达式对,每个都以圆括号闭合。来个示范比解释更容易理解:

user=> (defn square-or-multiply
"squares a single argument, multiplies two arguments"
([] 0)
([x] (* x x))
([x y] (* x y)))


    这里定义了有三个实现的函数,第一个实现参数vector为空,会被应用在无参调用函数时,它仅仅返回一个常量0.第二个实现拥有一个参数,返回这个参数的平方。第三个接收两个参数,返回它们的乘积。这可以在REPL中得到证实:

user=> (square-or-multiply)
0
user=>(square-or-multiply 5)
25
user=>(square-or-multiply 5 2)
10

 

 

参变量函数

 

    通常,使一个函数能够接收任意数量的参数是有必要的。这个被成为参数数量变量。为了适应这个需求,Clojure在函数定义的参数定义vector中提供了特殊符号&,fn和defn都可以使用它。


    使用时,只需要在参数vector中普通参数的后面加上&和一个符号名,然后之后的参数就可以被添加到一个序列中(跟list一样),这个序列会绑定在刚刚的符号名上面,举个例子:

user=> (defn add-arg-count
"Returns the first argument + the number of additional arguments"
[first & more]
(+ first (count more)))


    count是一个简单的内建函数用于计算一个list的长度,采用下面的代码调用一下:

user=> (add-arg-count 5)
5
user=> (add-arg-count 5 5)
6
user=> (add-arg-count 5 5 5 5 5 5)
10


    在第一次调用中,传递了简单的5绑定到first参数,空绑定到more,因为没有更多的参数了。(count more)返回0,所以执行的结果就是first的值5.然而在第二和第三次调用时,more分别绑定到list(5) 和(5 5 5 5 5),长度是1和5,这个值加上first的值就是函数的返回值。


    Chapter 4讨论了list和操作它的一些内建函数,这些函数都能够作用在more参数绑定的list上面。

 


简写 函数声明

 

    即使是像fn这样简洁地定义函数,总体来看,在某些情况下把函数定义全部写出来还是显得很累赘。具有代表性的例子就是声明内联使用的函数,而不是将函数绑定在一级符号上面。

   

    Clojure提供函数声明的简写,以宏读取器(reader macro)的形式。函数声明的简写形式,以井号(#)开头,后面跟一个表达式。这个表达式是函数的主体,里面包含的任意百分比符号(%)是函数的参数。


注意:宏读取器是专门的、简写的语法,通常仅仅被识别为Clojure的form,里面不会包含圆括号、方括号、花括号等等。在解析Clojure代码时,宏读取器会被首先处理,在代码编译之前被解析成对应的长form。在编译器看来,函数简写#(* %1 %2)实际上和长form(fn [x y] (* x y))是一样的。Reader macros仅仅被用来实现少些常见任务,而且不能被用户自定义。这个限制机制是为了防止Reader macros被过度使用导致代码的可读性变差,除非读取器和刚刚讨论的宏非常相似。避免用户自定义的宏读取器成为共享代码的阻碍,帮助Clojure的语义始终如一。尽管如此,它们在某些确定共用的场合下还是非常有用的,所以Clojure默认提供了一些宏读取器以供使用。


    举个例子,下面是个平方函数的宏读取器实现:

user=> (def sq #(* % %))
#'user/sq
user=> (sq 5)
25


    百分号表示函数接收了一个参数,并绑定到函数体内以供使用。要简写多个参数的函数声明,就需要在百分号后面加上1到20的数字:

user=> (def multiply #(* %1 %2))
'#user/multiply
user=> (multiply 5 3)
15


    %1或者%指向第一个参数,%2指向第二个,以此类推。这使函数声明看起来更加的简洁和清晰,尤其是在内联函数中:

user=> (#(* % %) 5)
25


    函数声明简写的最大缺点在于它可能造成阅读障碍,所以最好是在函数声明较短的情况下明智地使用简写。另外,注意函数声明简写是不能够嵌套的。

 

 

条件表达式


    任何程序都具备一个特性要点,就是根据不同的情况去改变状态。Clojure当然也提供一整套的简单条件form。


    最基础的条件form就是if form。它接收一个判断表达式作为第一个参数,如果表达式的值为true,它就返回第二个参数(then子句)的执行结果。如果表达式的值为false(或者nil),它就返回第三个参数(else子句)的执行结果。如果没提供第三个参数,就返回nil。举个例子:

user=> (if (= 1 1)
"Math still works.")
"Math still works."


    另一个例子:

user=> (if (= 1 2)
"Math is broken!"
"Math still works.")
"Math still works."


    Clojure同样提供if-not form。这个函数的使用方式同if一样,只是行为判断是相反的。如果表达式的值为false,返回第二个参数的执行结果;为true时则返回第三个参数的执行结果:

user=> (if-not (= 1 1)
"Math is broken!"
"Math still works.")
"Math still works."


    有时候,需要在多个条件之间选择而不仅仅是true或者false,你可以使用嵌套的if来实现,但是采用con form会更清晰一些。con可以以任意数量的 判断/表达式 对作为参数。在执行时,首先进行第一个判断,如果为true,就返回第一个表达式的值,如果为false,则接着执行剩下的判断。如果没有一个判断的值为true,则返回nil,除非你提供一个:rest form作为最后的表达式,担当捕获器。举个例子,下面的函数采用了con来发表对天气的评论:

(defn weather-judge
"Given a temperature in degrees centigrade, comments on the weather."
[temp]
(cond
(< temp 20) "It's cold"
(> temp 25) "It's hot"
:else "It's comfortable"))


    试着调用一下:

user=> (weather-judge 15)
"It's cold"
user=> (weather-judge 22)
"It's comfortable"
user=> (weather-judge 30)
"It's hot"


注意:cond是非常好用的,但是如果出现过大的cond是非常难以维护的,尤其是程序的可能情况不断增长的条件下。我们可以考虑采用多重方法(multimethods)的多态形式来代替它,这个将在Chapter 9谈到。多重方法就像cond一样实现条件逻辑,但是扩展性更强。

 

 

本地绑定

 

    在函数编程语言中,可以通过函数的组合--内嵌函数来获取新的值。然而,有时候我们需要对某次计算的结果指定一个名称,为了清晰和效率,因为这个值有可能被多次使用。


    Clojure为这种场景准备了let form。let可以让你一次指定多个符号的绑定,后面跟的表达式主体中就能使用这些绑定的符号。这些符号的作用域是本地(local)的,它们仅仅在let的主体范围内被绑定。它们同样是不变的,一旦它们被绑定,它们在let的主体范围内都一直是绑定的值而不能被任何改变。


    let form包含一个绑定关系的vector和一个表达式主体。绑定关系vector包含数个键值对。举个例子,下面我们用let将2绑定到a,3绑定到b,然后返回它们相加的结果:

user=> (let [a 2 b 3] (+ a b))
5


    这可能是最简单的let使用方式。然而,let能够进行比求值更加复杂得多的操作。让我们来看看接下来的例子,以了解什么时候使用let最为强力:

(defn seconds-to-weeks
         "Converts seconds to weeks"
         [seconds]
         (/ (/ (/ (/ seconds 60) 60) 24) 7))


    这个函数能够得到预期的结果,但是非常难以阅读。这些嵌套的除法函数有点让人迷惑,虽然大部分人都能够不太费事地理解它,但是表面上看,这么简单的功能不应该让函数形式这么复杂。因此,我们可以想象一个同样功能的函数,没有这么复杂的形式,写出来就不用去解释。

    我们采用let来清理一下这个函数:

(defn seconds-to-weeks
         "Converts seconds to weeks"
         [seconds]
         (let [minutes (/ seconds 60)
                 hours (/ minutes 60)
                 days (/ hours 24)
                 weeks (/ days 7)]
                 weeks))


    函数变长了,但是你能清晰地看到每步执行发生了什么。你绑定了临时符号在minutes, hours, days, 和weeks上,最终返回了weeks,而不是一次做完所有的运算。这个例子主要展示的是一个格式上的选择。它使代码更清晰,但是更长。什么时候使用let,如何去用,都取决于你。但是底线很清晰:采用let使你的代码更清晰,而且还能存储计算出来的中间量,这样你就不用去多次执行计算。

 

 

循环和递归

 

    用户们可能会感到一点点震惊,Clojure不直接提供循环语法,这可是编程语言的必要物啊。像其他函数编程语言一样,Clojure在需要重复执行某些代码的时候采用递归。因为Clojure鼓励使用不可变的数据结构,递归在概念上比典型的指令式迭代更合适。


    考虑到递归是个极大的挑战从指令式语言往函数式语言转换,但它出乎意料地强力和优雅,之后你会学到采用递归有多容易表达你的重复计算。


    大部分开发者对递归的最简单形式有一定概念--函数调用它自身。这是准确的,但是不足以描述递归实际带来的益处,也不明白怎样高效地运用它,更不了解它怎样运作于不同的场景之中。


    在Clojure(或者其他函数式语言,就此而言)中有效地运用递归,只需记住如下几条基本原则:

  • 用递归函数的参数去存储或改变计算的进程。在指令式程序语言中,循环通常通过不断修改一个计数器变量来不断进行。在clojure中,可没有计数器让你来改变。替代的解决方案是,充分利用函数的参数。不要老想着循环是不断重复改变些什么,而是函数的链式调用。每次调用需要包含接下来计算需要的所有信息。递归计算中修改的值和结果都需要作为参数传递给下次递归调用,以便持续修改。
  • 保证递归一定有个基本事件或者基本条件,在每次递归时,都要判断是否已经达到基本事件或者已经满足基本条件,如果判断为true,则结束递归并返回结果。这跟指令性语言中防止无限循环是一个道理。如果在代码中没有能够直接停止递归的情况,那么递归会一直继续。显然,这会出大问题。
  • 在每次迭代的过程中,递归语句执行的过程必须趋向于基本条件,否则,无法保证递归总会结束。代表性的,是采用一个不断增大或者减小的数字,每次执行时判断它是否到达某个作为基本条件的临界值。

    下面的例子,采用了牛顿的算法来计算某个任意数的平方根。这是个完整的,虽然很小的Clojure程序,包含了一个主函数和许多功能函数,来证明递归的以上特性:

(defn abs
         "Calculates the absolute value of a number"
         [n]
         (if (< n 0)
         (* -1 n)
         n))
(defn avg
         "returns the average of two arguments"
         [a b]
         (/ (+ a b) 2))
(defn good-enough?
          "Tests if a guess is close enough to the real square root"
           [number guess]
           (let [diff (- (* guess guess) number)]
           (if (< (abs diff) 0.001)
                 true
                 false)))
(defn sqrt
        "returns the square root of the supplied number"
        ([number] (sqrt number 1.0))
        ([number guess]
        (if (good-enough? number guess)
                guess
                (sqrt number (avg guess (/ number guess))))))

 

    让我们来试试这个函数,把上面源文件加载到Clojure运行环境之后,在REPL中执行:

user=> (sqrt 25)
5.000023178253949
user=> (sqrt 10000)
100.00000025490743


    就像预想的那样,代码返回了精确度在.001级别的任意数平方根。


    在这个源文件中首先定义的三个函数:abs, avg, 和 good-enough?,它们是直接的功能函数。现在还不用仔细了解它们,除非你自己愿意。戏肉是第四个函数,sqrt。


    sqrt函数最明显的是有两个实现,第一个可以被想成共用(public)接口。调用起来很简单,它只接收一个参数:你希望求平方根的那个数字。第二个是个递归实现,包含了两个参数:源数字和目前为止你最好的猜测值。第一个实现仅仅是用来调用第二个,以1.0为初始化猜测值。


    这个递归实现本身是简单的,它首先检查基础条件,通过已定义的good-enough?函数。如果你的猜测值足够接近真实的平方根,这个函数会返回true。如果达到了基础条件,函数就不会再迭代,而是返回猜测值。


    然而,如果还没达到基础条件,函数会继续调用自己进行递归。它将源数字和猜测值作为参数传递给下一次递归,作为计算的初始值。这实现了上面我们提及的递归函数的第一条特性。


    最后,注意表达式(avg guess (/ number guess))计算出的值提供给下次递归的guess参数。这个表达式总是计算出当前的猜测值和源数字除以当前猜测值的商的平均值,平方根的数学属性担保了每次计算出的猜测值都比上次要更接近实际源数字的平方根一些。这实现了一个好递归函数的最后一点需求--每次迭代让执行过程离结果越来越近。sqrt函数的每次迭代,都让猜测值离实际的平方根越来越近,最后终于近得足以通过good-enough?函数的验证而返回猜测值,结束递归。

 

    另一个递归的例子,用于计算N次方:

(defn power 
    "Calculates a number to the power of a provided exponent." 
    [number exponent] 
    (if (zero? exponent) 
        1 
        (* number (power number (- exponent 1)))))

 

    调用一下:

user=> (pow 5 3) 
125 

 

    这个函数采用递归的方式跟平方根又不一样。这儿运用到了数学表达式xn = x * x(n-1)。这个能够在递归调用中看到:函数返回一个数字,这个数字是最开始的数字和它的n-1次方相乘的结果。这儿有一个基本情况:如果指数为0,就返回1,因为x的0次方总是1。当你每次迭代将指数减1,总是能保证指数最终趋向于基本情况(除非你给出一个负指数)。


注意:当然,比起实现这些函数,有其他更容易的方式来求平方根或者进行指数运算。比如在java 的标准math类库中有对应的方法,Clojure很容易调用它们。上述两个函数仅仅是做为递归逻辑的一个单纯演示例子。Chapter 10 java 互用中有对调用java 方法的详细解说。

 

 

尾递归

 

    递归的一个实际问题在于,基于计算机物理硬件的局限性,对嵌套函数的层数是有限制的(取决于栈的大小)。在JVM上,这个数量是可以修改到很大的。在作者的机器上,这个数量大概是5000.然而,不管栈的长度有多大,它仍然导致了一个严重的问题:对函数的递归次数有着严格的限制。对于小函数来说,这个几乎不会有任何影响。

分享到:
评论
3 楼 Dead_knight 2013-02-26  
http://code.google.com/p/clojure-doc-en2ch/w/list
这里面第一章翻译的也太应付了吧,看的我想哭。
2 楼 songry 2012-01-06  
linkerlin 写道
标题里面的 未完 ,啥时候能去掉?

可以去这儿看
http://code.google.com/p/clojure-doc-en2ch/w/list
我参与到这里头的翻译去了
1 楼 linkerlin 2012-01-05  
标题里面的 未完 ,啥时候能去掉?

相关推荐

Global site tag (gtag.js) - Google Analytics