夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その5 2.5 フロー制御

なんか徐々にまともなプログラムを書くためのパーツがそろってきたように思える今日この頃です。
では、続きです。

Clojureではフロー制御の方式はif、do、loop/recurでほとんど賄える。

1.ifによる分岐

Clojureのifは最初の引数を評価し、それが論理的に真であれば2つ目の引数を評価した結果を返す。
真でなかった場合は3つ目の引数が存在すれば3つ目の引数を、存在しなければnilを返す。

user=> (defn is-small? [number]
    (if (< number 100) "yes"))
#'user/is-small?
user=> (defn is-small-withno? [number]
    (if (< number 100) "yes" "no"))
#'user/is-small-withno?
user=> (is-small? 100)
nil
user=> (is-small? 99)
"yes"
user=> (is-small-withno? 99)
"yes"
user=> (is-small-withno? 100)
"no"

ifを応用して作られたwhen/when-notもあるらしいが、またそれは別の機会に。

2.doによる副作用の導入

上記のifではそれぞれの分岐先に1つの式しか書けない。
分岐後に複数の関数を実行したい場合などが厄介になる。
その場合はdoを使用する。

user=> (defn is-small-print? [number]
    (if (< number 100)
        (do
            (println "Inputed Small Nimber " number)
            "yes")
        (do
            (println "Inputed Big Number " number)
            "no")))
#'user/is-small-print?
user=> (is-small-print? 100)
Inputed Big Number  100
"no"
user=> (is-small-print? 99)
Inputed Small Nimber  99
"yes"

doは最後のフォーム以外は実行するだけで返り値には影響を及ぼさない。
つまりは最後のフォーム以外が意味を持つためには「何かしらの外部への作用」が必要となる。
#今回の場合、println
これは返り値とは直接関係のない処理となる。これを「副作用」と呼ぶ。
Java等では副作用があるなしはごちゃまぜに使用されるが、Clojureでは副作用は明示される必要があるし、多く使いもしないというのが基本。
ちなみに、do節は別にifとは関係なく使用可能。

user=> (defn square [number]
    (do
        (println "Inputed" number)
        (* number number)))
#'user/square
user=> (square 10)
Inputed 10
100

3.loopとrecurによる再帰

loopは下記のようにして制御構造を用いる。
(loop [bindings *] exprs*)
これだけ見るとletと変わらないが、loopの場合recur特殊形式のターゲットとなる再帰ポイントを作る・・・らしい。
下記のように指定することで、recurはloopのバインドに新しい値を与えて、制御をloopの頭に戻す。
(recur exprs*)
これだけだとさっぱりのため、試してみる。

user=> (defn create-decr-array [number]
    (loop [result [] num number]
        (if (zero? num) result
        (recur (conj result num) (dec num)))))
#'user/create-decr-array
user=> (create-decr-array 100)
[100 99 98 97 96 95 94 93 92 91 90 89 88 87 86 85 84 83 82 81 80 79 78 77 76 75
74 73 72 71 70 69 68 67 66 65 64 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48
 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32 31 30 29 28 27 26 25 24 23 22 2
1 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1]
user=> (class (create-decr-array 100))
clojure.lang.PersistentVector

まずはじめにloopはresultに空ベクタ、numに引数として与えられた数をバインドする。
その後、recur節でresultにnumの値を追加し、numをデクリメントする。
そのresult、numの状態でrecur節はloopの頭から再度処理を実行する。
結果、上記のような結果となる。

尚、loopがない場合はrecurは関数の頭に返るため、関数全体を暗黙のloopとするようなコードも書ける。

user=> (defn count-down [result number]
    (if (zero? number) result
        (recur (conj result number) (dec number))))
#'user/count-down
user=> (count-down [1] 10)
[1 10 9 8 7 6 5 4 3 2 1]
user=> (count-down [1 2] 10)
[1 2 10 9 8 7 6 5 4 3 2 1]
user=> (count-down [] 10)
[10 9 8 7 6 5 4 3 2 1]

こちらの方が「recur節の中で引数であるresult、numberを再バインドしているということが明確なため、
微妙に分かりやすい気がする。
・・・loopだとバインド対象の変数名と変数値がごちゃまぜになっているというのもあるが。

尚、別にrecurを使用しなくてもiterate/reverse等のシーケンスライブラリを使えば実現できる模様。
ともあれ、それはまた別の話。
ただ、recurにこだわる必要がないということだけは覚えておく必要があるとのこと。