clojure初心者ガイド(16):基本反復&再帰


反復と再帰は2つの異なる概念であるが,それらは互いに少し似ている.反復は、要素のセットを遍歴し、遍歴中に各要素に対して対応する操作を行い、再帰的には自分で自分を呼び出す操作を実行します.
再帰と反復の概念から見ると、これは完全に2つの全く異なるものであり、それらの類似性はどこに現れているのだろうか.まず再帰は集合要素を遍歴する方法としてもよく,Clojureには再帰方式の反復器がある.この章では、clojureでの反復と再帰の動作方法とそれらの使用のメリットを明らかにします.
doseqを使用した反復
まず、この問題を解決するために反復を使用する必要がある問題の例を見てみましょう.この例の問題はFizzzBuzz難題と呼ばれています.
プログラムを書いて1から100の数字を印刷します.ただし、数字が3の倍数になった場合は、数字の代わりに「Fizz」、5の倍数の代わりに「Buzz」、3の倍数で5の倍数の代わりに「FizzBuzz」を印刷します
解決を始めましょう.
テーマの分析から分かるように、まず1から100までの数字の中で3、5を除いて、3を除いて5を除いてもよい数字を確定しなければならないので、少なくとも1つの判断を下す関数が必要で、multipleと呼んでもいいですか?(疑問符で終わる関数名は一般的にブール値を返します).clojureの内蔵余剰関数modを使用してmultiple?関数を作成できます.
=>(defn multiple? [n div]
    ;; n    div       0 
   (= 0 (mod n div)))
#'user/multiple

;; 3  3  ,  true
=>(multiple? 3 3)
true

;;  4  3  ,  false
=>(multiple? 4 3)
false

;;  5  3  ,  false
=>(multiple? 5 3)
false

;;  6  3  ,  true
=>(multiple? 6 3)
true

今では除去を判断する関数multipleがありますか?FizzzBuzz問題の具体的な処理に着手することができます.Clojureの世界では、要素を巡る方法がいくつかあります.次に、「doseq」(マクロラベル)を使用して反復操作を行います.シーケンス内の要素を巡回し、巡回中に対応する処理を行います.私たちがdoseqに与えた最初のパラメータは、現在の要素をバインドする変数名(アルファベットiを使用する)と遍歴されたシーケンスを含むベクトル(vector)であるべきです.doseqの2番目のパラメータは、各要素を遍歴する操作式(s式)です.
まず簡単な例を見てみましょう.
;;  0 9   

user=> (doseq [i (range 0 10)] 
              (println i))
0
1
2
3
4
5
6
7
8
9
nil

ネスト反復(ネストforループのようなもの)を見てみましょう
user=> (doseq [ x [1 2 3]
                y [1 2 3]]
              (println (* x y)))

1
2
3
2
4
6
3
6
9
nil

上のコードと下のjavaコードは基本的に等価です(すべてのclojure式には戻り値があり、上のコードの最後のnilが戻り値です):
int [] array = {1, 2, 3};
for(int i : array){
  for(int j : array){
     System.out.println( i * j );
  }
}
doseqの紹介はこれで終わります.doseqを使用してFizzBuzzの問題を解決する方法を見てみましょう.
=>(doseq [i (range 1 101)]  ;;  1 100
     ;;          5 3  
     (cond (and (multiple? i 3)(multiple? i 5))
             (println "FizzBuzz")
           ;;              3  
           (multiple? i 3)
             (println "Fizz")
            ;;             5  
           (multiple? i 5)
             (println "Buzz")
        ;;         
       :else (println i)))
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
  .......
強調点:condの使用はif elseによく似ており、condは上から下へ順に式の真の値を判断し、条件式の真の値がtrueであれば、その条件式に対応する実行式の値を返し、今回の判断が終了すると、最終的にelse文に実行されるまで次の判断文が実行される.
;;cond     

(cond (     1) (     1)
      (     2) (     2)
               ......
       :else (     n)
) ;;cond   

forを使用して反復
forループは別の反復ですが、forループを使用するとFizzBuzz問題を解決するのに適していないことがわかります.forループの構文はdoseqと同じですが、forはlazy seq(pythonのyieldに似ています)を返し、doseqはside effectです.抽象的ですが、例で説明しましょう.
;;       0-10      ,        nil
user=> (doseq [x (range 0 11) :when (even? x)] x)
nil 

;;   doseq    nil,                。     
user=> (doseq [x (range 0 10) :when (even? x)] (print x ","))
0 ,2 ,4 ,6 ,8 ,nil  ;; (nil          ,     )

;;    for   0-10      
user=> (for [x (range 0 10) :when (even? x)] x)
(0 2 4 6 8) 


このようにdoseqを使用するとjava内のforループに向かい、ループ中にしか何もできないが、clojure内のforループは各ループで値を外部に出力し、最終的にこれらの値からシーケンスを構成することができる.
もう一つの例で体得する
user=> (for [x [0 1 2 3 4 5]
             :let [y (* x 3)]
             :when (even? y)]
         y)
(0 6 12)  ;;       

forループがFizzzBuzz問題を解決するのに適していない理由は、FizzBuzzがループ中に対応する値を印刷する必要があるだけで、毎回結果を返す必要がないからです.興味があれば、FizzzBuzzコードのdoseqをforに変えて出力効果を見てみるとわかります.
loopによる再帰
loopは多くの言語でこのキーワードがあり、基本的には反復器をよりよく使用するために存在します.しかしClojureではloopは実際に再帰的であるため,より多くの関連知識とコードが必要である.
まずloopを使ってFizzzBuzz問題を解決する方法を見て、体得してみましょう
(loop [data (range 1 101)]
  (if (not (empty? data))
      (let [n (first data)]
        (cond (and (multiple? n 3)(multiple? n 5))
                     (println "FizzBuzz")
                   (multiple? n 3)
                     (println "Fizz")
                   (multiple? n 5)
                     (println "Buzz")
                 :else (println n))
             (recur (rest data)))))
まずcondの中の論理は前のdoseqとそっくりで、これは変わらない.再帰には終了条件が必要であることを知っているので、ここで再帰開始に判断文(if(not(empty?data))を追加しました.これは、dataが空のリストであるかどうかを判断し、空の再帰終了である場合、そうでない場合、継続します.再帰するたびに、リストから値を取り出し、condの論理に渡して判断します.condロジックが終了すると,上記のロジックを再帰的に呼び出すためにrecurを用いて目的を達成する.上記の例では、毎回、今回の再帰のリストを使用して、最初の要素以外の残りのリストを使用して次の再帰を行います.(再帰は収束するプロセスでなければなりません.そうしないと、再帰は永遠に終わりません)
loopを使用して0-11の偶数を印刷し、前の例と比較します.主に再帰思想をどのように使って問題を解決するかを体得する.
user=> (loop [x 0]
(when (<= x 10)   ;;           
 (if (even? x) (println x))
 (recur (+ x 1))))  ;;  recur          
(『the little schemer』を見てみると、再帰的な考えをよりよく身につけることができ、clojureを学ぶのに大きなメリットがあることをお勧めします)
次に、少し難しい例を示します.数値のセットを再帰的に反復し、遍歴中に得られた最初の10個の偶数を収集します.この例は前とは異なり,我々が再帰(recur)するたびに伝達するパラメータは1つではなく複数であることに注意する.recurの後のパラメータは実はloopの最初のベクトルパラメータのバインドパラメータ(data,n,n-count,result)と一つ一つ対応しているので、よく観察してみましょう.
(loop [data   (range 1 101)
       n      (first data)
       n-count   0
       result  nil]  ;; result       
   (if (and n (< n-count 10))   ;;      
       (if (even? n)
           (recur (rest data) (first data) (inc n-count) (cons n result))
           (recur (rest data) (first data) n-count result))
       (reverse result))) ;;     ,      

より良いことは、上記を再帰関数として定義することです.
=>(defn take-evens                       ;;         
    ([x nums](take-evens x nums 0 nil))  ;;     
    ([x nums n-count result]             ;;     
      (if (empty? nums)                  ;;       
          (reverse result)     
          (if (< n-count x)              ;;        
              (let [n (first nums)]
                (if (even? n)
                    (recur x (rest nums) (inc n-count) (cons n result))
                    (recur x (rest nums) n-count result)))
              (reverse result)))))
#'user/take-evens

;;  1 100      
=>(take-evens 10 (range 1 101))
(2 4 6 8 10 12 14 16 18 20)
;;  1 100  5   
=>(take-evens 5 (range 1 101))
(2 4 6 8 10)