Clojure-JVM上的函数式编程语言(4)程序流控制 作者: R. Mark Volkmann

 原帖地址:http://java.ociweb.com/mark/clojure/article.html#ConditionalProcessing

 作者:R. Mark Volkmann

 译者:RoySong

 

程序流控制

条件判断

    特殊form if会检验一个条件,然后根据检验结果来决定执行两个表达式中的哪一个。

它的语法是(if condition then-expr else-expr ),其中的else部分( else-expr )是可选的。

如果then部分或者else部分需要不止一个表达式,则采用特殊form do来将它们包装成一个表达式。

例子如下:

(import '(java.util Calendar GregorianCalendar))
(let [gc (GregorianCalendar.)
      day-of-week (.get gc Calendar/DAY_OF_WEEK)
      is-weekend (or (= day-of-week Calendar/SATURDAY) (= day-of-week Calendar/SUNDAY))]
  (if is-weekend
    (println "play")
    (do (println "work")
        (println "sleep"))))

 

    when和 when-not在只需要一种分支的情况下可以替代if,它们的方法体中可以包含任意数目的表达式,而不用do来包装。

例子如下:

(when is-weekend (println "play"))
(when-not is-weekend (println "work") (println "sleep"))

 

    if-let宏将一个值绑定到某个符号上,然后根据这个值的逻辑真或者假(在 "Predicates "这一节中解释

来决定执行哪个表达式。下面的代码会打印出在队列中等待的第一个用户的名字,如果队列中没有等待的用户,

则打印出"no waiting"。

(defn process-next [waiting-line]
  (if-let [name (first waiting-line)]
    (println name "is next")
    (println "no waiting")))

(process-next '("Jeremy" "Amanda" "Tami")) ; -> Jeremy is next
(process-next '()) ; -> no waiting
 

    when-let宏跟if-let宏很类似,它们的不同和if跟when的不同方式一样。

when-let不支持else部分,并且when部分可以包含任意数目的表达式。例子如下:

(defn summarize
  "prints the first item in a collection
  followed by a period for each remaining item"
  [coll]
  ; Execute the when-let body only if the collection isn't empty.
  (when-let [head (first coll)]
    (print head)
    ; Below, dec subtracts one (decrements) from
    ; the number of items in the collection.
    (dotimes [_ (dec (count coll))] (print \.))
    (println)))

(summarize ["Moe" "Larry" "Curly"]) ; -> Moe..
(summarize []) ; -> no output

 

    condp宏跟其他编程语言的case声明类似。它的参数包含一个双参数断言(通常采用=或者 instance? ),

一个表达式作为第二个参数,后面紧跟任意数量成对的值/结果表达式。这些成对值/结果中的值会被依次传入

断言中,如果断言执行结果为真,则返回对应表达式执行的结果;如果断言执行结果为假,则继续下一个

值/结果的断言验证。最后还可以定义一个可选的参数,在如果所有的值/结果断言验证都为假的情况下,

来返回一个值。如果没定义这个可选参数,而所有的断言验证都为假,就会抛出一个

IllegalArgumentException

 

    下面的例子展示了如果用户输入数字1,2,3时就会打印出对应的名字,其他情况,则会打印出

"unexpected value"。在这之后,会检查本地绑定"value"的类型,如果是number类型,就会打印出这个

数字的2倍值;如果是String类型,则会打印出这个字符串长度的2倍值。

(print "Enter a number: ") (flush) ; stays in a buffer otherwise
(let [reader (java.io.BufferedReader. *in*) ; stdin
      line (.readLine reader)
      value (try
              (Integer/parseInt line)
              (catch NumberFormatException e line))] ; use string value if not integer
  (println
    (condp = value
      1 "one"
      2 "two"
      3 "three"
      (str "unexpected value, \"" value \")))
  (println
    (condp instance? value
      Number (* value 2)
      String (* (count value) 2))))
 

    cond宏接收任意数量的成对断言/结果表达式,它会依次对每个断言求值,直到某个断言的值为真,

则返回断言对应的结果。如果没有任何断言的值为真,则会抛出 IllegalArgumentException

通常最后一个断言会简单地写为true,来对应生下的所有情况。

 

    下面的例子是由用户输入水温,程序会打印出水是冰的、烫的或者其他。

(print "Enter water temperature in Celsius: ") (flush)
(let [reader (java.io.BufferedReader. *in*)
      line (.readLine reader)
      temperature (try
        (Float/parseFloat line)
        (catch NumberFormatException e line))] ; use string value if not float
  (println
    (cond
      (instance? String temperature) "invalid temperature"
      (<= temperature 0) "freezing"
      (>= temperature 100) "boiling"
      true "neither")))

 迭代

    有很多种方式来进行“循环”或者对集合中的元素进行迭代。

 

    dotimes宏会将方法体中的表达式重复执行指定的次数,指定给本地绑定的执行序列值从0开始一直到次数减一。

如果这个本地绑定(比如下例中的card-number )是不需要的,则可以采用下划线做为占位符。例子如下:

(dotimes [card-number 3]
  (println "deal card number" (inc card-number))) ; adds one to card-number
 

    注意采用了inc函数,所以输出值由0,1,2变成了1,2,3.上面代码的输出如下:

deal card number 1
deal card number 2
deal card number 3
 

    while宏会在它的判断表达式( test expression)为true时一直执行方法体中的表达式。

下面的例子会在指定线程保持运行的过程中一直执行while主体中的内容:

(defn my-fn [ms]
  (println "entered my-fn")
  (Thread/sleep ms)
  (println "leaving my-fn"))

(let [thread (Thread. #(my-fn 1))]
  (.start thread)
  (println "started thread")
  (while (.isAlive thread)
    (print ".")
    (flush))
  (println "thread stopped"))
 

    这段程序的输入大致如下:

started thread
.....entered my-fn.
.............leaving my-fn.
thread stopped

 列表解析

    for和doseq宏能够执行列表解析。它们支持遍历多重集合(最右的集合最快),还可以选择:when和:while表达式来进行过滤。for宏以一个单独的表达式做为执行主体,返回一个延迟序列结果。 doseq宏以一系列任意数量的表达式做为主体,依次执行以获得它们的副作用,然后返回nil。

 

    下面的例子是遍历一张表格打印出遍历到的单元格,先逐行遍历,在每行中又逐列遍历。中间跳过了B这一列且只遍历小于3的行。注意其中的dorun宏,被用来强制对for返回的延迟序列求值,其详细用法将在"Sequences "这一节描述。

(def cols "ABCD")
(def rows (range 1 4)) ; purposely larger than needed to demonstrate :while

(println "for demo")
(dorun
  (for [col cols :when (not= col \B)
        row rows :while (< row 3)]
    (println (str col row))))

(println "\ndoseq demo")
(doseq [col cols :when (not= col \B)
        row rows :while (< row 3)]
  (println (str col row)))
 

    上面代码产生以下结果:

for demo
A1
A2
C1
C2
D1
D2

doseq demo
A1
A2
C1
C2
D1
D2

 

    特殊form loop,就像名字所展示的那样,支持循环(loop)。

它以及和它合作的特殊form recur都将在下节中进行说明。

 

递归

    递归是指当一个函数直接或者间接通过其他函数调用自身的情况。

递归中止的条件通常是某个集合元素变空或者某个数字已经达到了指定的值。

前一种情况通常是持续采用next函数不断返回除去了头元素的集合来实现,

而后一种情况则通常是采用dec函数 不断对某个数字持续相减来实现的。

 

    在递归时,如果调用栈层次太深,有可能出现运行时内存溢出的情况。

某些程序语言会采用"tail call optimization " (TCO)的方式来处理这个问题,

java不支持,但是Clojure支持。

在Clojure中避免内存溢出的一种方式是采用loop和recur特殊form,

另外一种是采用 trampoline 函数。

 

    loop /recur的约定组合看起来象是在循环中调用递归,但并不消费栈空间。

特殊form loop和特殊form let相似的地方就在于,它们都是建立本地绑定,

但loop同时会建立一个递归节点以供recur来进行调用。loop在创建本地绑定

时会为它指定一个初始值。接下来的recur调用完成后会将控制权交还给loop,

并为本地绑定指定一个新的值。被传递给recur的参数数量必须和loop建立的

绑定数量相同,同样,recur只能出现在loop调用的结尾处。

(defn factorial-1 [number]
  "computes the factorial of a positive integer
   in a way that doesn't consume stack space"
  (loop [n number factorial 1]
    (if (zero? n)
      factorial
      (recur (dec n) (* factorial n)))))

(println (time (factorial-1 5))) ; -> "Elapsed time: 0.071 msecs"\n120
 

   defn宏,就跟loop特殊form一样,也建立了一个递归点。

recur特殊form也可以放到函数的末尾处来为函数传递一个新的值来进行递归调用。

 

    实现阶乘的另外一种方法是采用reduce函数,它在之前的 "Collections "章节已经描述过了。

它看起来更加函数化,更少指令式编程的样式。不幸的是,在当前例子中,它会更低效。

注意range函数接收了一个包含的低层绑定( lower bound)和一个未包含的高层绑定( upper bound)。

(defn factorial-2 [number] (reduce * (range 2 (inc number))))

(println (time (factorial-2 5))) ; -> "Elapsed time: 0.335 msecs"\n120
 

    采用apply替换掉reduce也能获得相同的结果,但那会花销更长的时间。

这说明了在选择函数时了解它们特征的重要性。

 

    recur特殊form并不适用于a函数调用b函数,b函数再调用a函数这种 相互递归 的情况。

而这篇文章没提到的 trampoline 函数更适合相互递归一些。

 

条件判断

    Clojure提供了很多函数来检验某个条件,执行判断。它们的返回值都可以解读为true或者false。

false和nil值都会被解读为false。true和其他任何值,包含0,都会被解读为true。

条件判断函数通常采用?结尾。

 

    反射调用包含了对象的信息,不仅有值,还包括类型等。有很多条件判断函数都是执行的反射。

检验单一对象类型的判断函数有:class? , coll? , decimal? , delay? , float? , fn? , instance? ,

integer? , isa? , keyword? , list? , macro? , map? , number? , seq? , set? , string?和 vector? 。

而某些执行了反射的非判断函数有: ancestors , bases , class , ns-publics和 parents。

 

    检验值之间关系的判断函数有: < , <= , = , not= , == , > , >= , compare , distinct?和 identical?。

 

    检验逻辑关系的判断函数有:and , or , not , true? , false?和 nil?。

 

    检验序列的判断函数大部分在之前我们都遇到过了,包括:empty? , not-empty , every? ,

not-every? , some和 not-any?。

 

    检验数字的判断函数有: even? , neg? , odd? , pos?和 zero?。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值