* [2022-09-14 水]
((☼ . 晴れ) (☼☁ . 晴れ 時々 くもり) (☼☁ . 晴れ 時々 くもり))
** 空芯菜の収穫 :agriculture:

** common-lispでfizzbuzz :lisp:

とあるとこで見掛けたので、
とりあえず他の人のやり方とかは見ず
まっさらな心で書いてみた。

*** 自分の書いたfizzbuzz

 #+BEGIN_SRC lisp
   ;; 1から100までの数をプリントするプログラムを書け。
   ;; ただし3の倍数のときは数の代わりにFizzと
   ;; 5の倍数のときはBuzzとプリントし、3と5の両方の倍数の
   ;; 場合にはFizzBuzzとプリントする事。
   (defun fizzbuzz (n)
     (loop :for i :from 1 :to n
           :do (let ((lcm (lcm 3 5)))
                 (cond ((zerop (mod i lcm)) (print 'FizzBuzz))
                                            ((zerop (mod i 5)) (print 'Buzz))
                                            ((zerop (mod i 3)) (print 'Fizz))
                                            (t (print i))))))      ; =>FIZZBUZZ 

 #+END_SRC


 昔からよく見掛けるし、エンジニアの面接試験だかで
 よく出てくるとか聞くけど、あんまりやった事ない気がする。
 common lispはなんでもloopで書けるから楽。
 でもlispらしくないと言えばらしくない。

 他の人はどう書いているんだろう?と思ってネットを
 調べてみる。

*** Common Lisp で Fizz Buzz

 #+BEGIN_SRC lisp
   ;; #1
     (dotimes (i 100)                     ; マクロ. (dotimes (カウンタ変数 繰り返し数 dotimesの返り値) E0 E1 ...)
       (let (num)                         ; (let (V0 V1 V2 ...) E0 E1 E2 ...) 局所変数を定義する
         (setq num (+ i 1))               ; num に i + 1 を代入
         (if (= 0 (mod num 3))            ; Fizzを判断
           (format t "Fizz")              ; Fizzを出力
           nil)                           ; else節(なにもしない)
         (if (= 0 (mod num 5))            ; Buzzを判断
           (format t "Buzz")              ; Buzzを出力
           nil)                           ; else節(なにもしない)
         (if (and (not (= 0 (mod num 5))) ; FizzでもBuzzでもないのを判断
                  (not (= 0 (mod num 3))));
           (format t "~a" num))           ; 数字を出力
         (format t " " )))                ; すきま
   ;; #2
   ; counter 個のインクリメンタルなリストを作る
   (defun iota (counter &optional (start 0))
     (if (= counter 0)
       nil
       (cons start (iota (- counter 1)
                         (+ start 1)))))
   ; FizzBuzz を返す
   (defun fizz_buzz (num)
     (cond ((and (= 0 (mod num 3)) (= 0 (mod num 5))) "FizzBuzz")
           ((= 0 (mod num 3)) "Fizz")
           ((= 0 (mod num 5)) "Buzz")
           (t num)))
   ; 出力
   (format t
           "~{~a ~}"
           (mapcar #'fizz_buzz (iota 100 1)))
 #+END_SRC

 #1はdotimesを書いてて逆にその発想が最近薄くなっていると自分で
 感じた。見た目はなんとなくlispっぽいんだけど、
 ちょっとわかりづらい。他人の書いたソースコードだから
 ってのもあるのかもだけど、if文を3つも使ったり、
 その都度nilを入れてきたり、最後に隙間のformatを別途
 使ったり、ちょっと冗長な感じもする。
 ごめんなさい、批判したい訳じゃなく
 あくまで他人のコードを参考に学ぶつもりで書いてます。
 まぁ、素人って自分で書いているし、
 素人で動くもの書けたんだから凄いと思う。
 andを使ってるのもなるほどそういう書き方もあるなと感じた。

 #2でiotaを作っているのは自分もちょっと考えた。
 数列を作る時はiotaみたいな関数があれば便利かなと。
 ただloopの場合、もうそういうものも要らなくなるから
 敢えて作らなかった。
 iotaを再帰で書いてるけど、lispっぽいし、
 うまいなぁと。loopを多用している事もあって、
 再帰的な考えがちょっと難しくなってると反省。
 頭悪いからパッと理解出来ないんだよな笑

 おお!しかもこっちはバシッとcondでまとめてきた。
 #1は何だったんだ?って感じの書きっぷりだよな笑

 ただ、最後はmapcarした後、
 formatでループをかけてる。2回回してる。
 iotaも入れると3回。これは遅くならないかな?
 問題的にはどうなんだろ?括弧付きでいいなら
 mapcarまででもいいような気もするんだけど。
 きちんと出力するところまでやるなら、
 やっぱりfizzbuzz関数内で出力した方がいい気がした。

 次見つけたのはこれ。

*** Common Lispで最速のfizzbuzzを実装した話

 #+BEGIN_SRC lisp
   (defmacro fizzbuzz (n)
     (format nil "~{~a ~}" (loop for i from 1 below n
                                 collect (cond
                                           ((= 0 (mod i 15)) "fizzbuzz")
                                           ((= 0 (mod i 5)) "buzz")
                                           ((= 0 (mod i 3)) "fizz")
                                           (t i)))))

   (defun main ()
     (print (fizzbuzz 100)))
 #+END_SRC

 これもloopやった後にformatでまたループやってるなぁ。
 ただよく見るとdefmacro使ってるな笑
 正直速度はあまり拘って作ってはいないけど、
 ちょっと自分の書いたものと処理速度比べてみるか。
 結果は以下の通り。

*** それぞれの速度
 ;; 自分の書いたもの
 Evaluation took:
   0.000 seconds of real time
   0.000042 seconds of total run time (0.000036 user, 0.000006 system)
   100.00% CPU
   147,778 processor cycles
   0 bytes consed
 
 ;; defmacroバージョン 
 Evaluation took:
   0.000 seconds of real time
   0.000009 seconds of total run time (0.000008 user, 0.000001 system)
   100.00% CPU
   29,156 processor cycles
   0 bytes consed
  
 確かに速いし、負けてる。

 ;; ちなみに先程のiotaバージョンは
 Evaluation took:
   0.000 seconds of real time
   0.000043 seconds of total run time (0.000043 user, 0.000000 system)
   100.00% CPU
   153,587 processor cycles
   0 bytes consed

 結果自分のものとさほど大差なし。

 自分の書いたものをただ単純にdefmacroにしてみた。
 結果は以下の通り。早っ!

 Evaluation took:
   0.000 seconds of real time
   0.000001 seconds of total run time (0.000001 user, 0.000000 system)
   100.00% CPU
   1,813 processor cycles
   0 bytes consed

 やはりprocessor cyclesが違うね。
 速さに拘るんだったらlcmも消すな。
 で、得意じゃないけど、最適化とかするだろうな。
 で、やってみた結果がこれ

 Evaluation took:
   0.000 seconds of real time
   0.000002 seconds of total run time (0.000002 user, 0.000000 system)
   100.00% CPU
   1,628 processor cycles
   0 bytes consed
  
 うーん、processor cyclesは下がったけど、
 run timeは増えている。
 もうこのレベルだといじったところで
 大差ない。

 逆にdefunで最適化したらどうなるんだろう?
 型の最適化追加
 Evaluation took:
   0.000 seconds of real time
   0.000049 seconds of total run time (0.000049 user, 0.000000 system)
   100.00% CPU
   177,785 processor cycles
   0 bytes consed

 あれ?最適化前より増えてるぞ笑
 optimizeも入れてみた。
 Evaluation took:
   0.000 seconds of real time
   0.000045 seconds of total run time (0.000037 user, 0.000008 system)
   100.00% CPU
   158,064 processor cycles
   0 bytes consed

 そんな速くなる訳でもないなぁ。
 とりあえずここでは効果なしだな。

*** defmacro効果が凄い。
 ただなんだろう。自分も一時期defmacro書きまくっていたけど、
 defmacroを更にdefmacroしたりしてたらエラーが出て、
 lisperの方に訊いたら、defunで書けるものは
 なるべくdefunで書くように言われた。

 それからはどうしてもdefunで書けないところだけに絞るようにしてる。

 でも勉強になったなぁ。
 まだまだ検索に引っかかってきたけど、もうお腹いっぱいなので、
 やめておく。

 いや、リアルの方はむしろお腹空いたけど笑

 追記:過去の自分の練習した内容をgrepしてみたら、
 2019-0322-201724.lispというのが見つかって、
 そこで例の最速のfizzbuzzを一度自分で試してみてたようだ。
 defmacroだとcompile時に計算して、
 実行時はその計算結果を書き出すだけって自分で書いてある。
 なるほど、それで速いわけだ笑