komorebikoboshiのブログ

プログラミング記事(趣味レベル)が多め。

Common LispでBMIを求めてみる

基礎プログラミング演習(1) - 技術memoBMIを求める問題をやってみました。せっかくなのでよく知らないプログラミング言語を使ってみようと思い、また、最近On Lispを読み始めて興味がわいていたのでCommon Lispを使ってみることにしました。

演習問題1

とりあえず必要なのは入出力です。出力に関しては、format関数がC言語における(f)printf関数のような感じなのでそれを使っていきます。入力に関しては、

  • read
  • read-lineとread-from-stringを組み合わせる

という二通りの方法があるみたいですが、readだと入力文字列ににスペースが含まれていた場合に読み込みがヘンになることがあるようなのでread-lineの方を使っていこうと思います。
とりあえずメインの処理はこんな感じでしょうか。

(defun bmi-1 ()
  (let ((height) (weight))
    (progn
      (format t "身長は?~%")
      (setq height (/ (read-from-string (read-line)) 100.0))
      (format t "体重は?~%")
      (setq weight (read-from-string (read-line)))
      (format t "あなたのBMIは~Fです~%" (/ weight (* height height))))))

メモ

  • letでローカル変数を定義。初期値を入れなくてもよい
  • prognで式を連続実行
  • ふつうに / で割ると分数(!)になる。小数で割ると結果が小数になる
  • formatの第1引数に t を入れるとコンソールに出力。文字列中の~Fは実数型の指定。~%は改行になる。ちなみに~Aだったら何でも表示できる。~を表示したい時は「~~」

で。問題文には「exeファイルを実行する」とあります。lispというとインタプリタなイメージが強いですが、実行形式のファイルにコンパイルできる処理系もあるようです。私が使っているClozure CLもその中のひとつです。とりあえずソースコード(bmi1.lisp)にこんなことを書いておき、

(ccl:save-application "bmi1.exe" ;生成するファイル名
                      :toplevel-function #'bmi-1 ;最初に実行する関数。
                      :prepend-kernel t) ;なんか書いとく

こんな感じでコンパイルします。

wx86cl -K CP932 --no-init --load bmi1.lisp

演習問題1-1

前問とほとんど同じなのでいきなり全体を貼ります。

(defun bmi-1-1 ()
  (let ((height) (weight) (bmi))
    (progn
      (format t "身長は?~%")
      (setq height (/ (read-from-string (read-line)) 100.0))
      (format t "体重は?~%")
      (setq weight (read-from-string (read-line)))
      (setq bmi (/ weight (* height height)))
      (format t "あなたのBMIは~Fです~%" bmi)
      (format t "あなたは~A体型です" (cond
                                       ((< bmi 18.5) "痩せ")
                                       ((<= 25 bmi) "肥満")
                                       (t "普通"))))))

(ccl:save-application "bmi1_1.exe"
                      :toplevel-function #'bmi-1-1
                      :prepend-kernel t)

違いは計算結果を変数bmiに代入することにしたことと、最後の表示をcond式で切り替えるところ。cond式は上から順番に条件が真になるか調べていく。最後に t (常に真)を置いておくことで、どの条件にもかからなかったものがここでキャッチされる。

演習問題1-2

まずはループの方法。そのものズバリなloop文を使うのが簡単かな。loop文は無限ループを作る。その中でreturnを呼び出してやるとループから抜ける。(関数から抜けるわけではない)

(let ((x 10))
  (loop
       (if (= x 0)
           (return x)) ;returnに渡した値がloopの評価値になる
       (print x)
       (setq x (- x 1))))

あとは入力が妥当かどうかのチェック。文字列から整数への変換にはparse-integerという関数があるけどこれは失敗したら例外を発生させるので、今回はやめておく。*1結局read-from-stringで読み取ってからintegerp関数で整数かどうかを判断するのが早いかなあ。
ただこれには問題があって、例えば "123 abc" のように間にスペースが空いた文字列を入力されると、最初の123だけが読み込まれて妥当な入力だと判断されてしまう。どうしよう、と考えたけど単純にスペースの含まれる文字列ははじくことに。で、まとまりごとに関数に切り分けると

(defun to-natural-number (str)
  (if (search " " str)
      nil
      (let ((value (read-from-string str)))
        (if (and (integerp value)
                 (< 0 value))
            value
            nil))))

(defun ask (question)
  (format t "~A~%" question)
  (let ((input) (n-number))
    (loop
       (setq input (read-line))
       (setq n-number (to-natural-number input))
       (unless (eq n-number nil) (return n-number))
       (format t "エラー。0より大きい整数を入力してください~%"))))

(defun calc-bmi (height weight)
  (let ((height-m (/ height 100)))
    (float (/ weight (* height-m height-m)))))
  • 関数to-natural-numberは失敗したときはnilを返す。
  • 関数askは質問文を引数で受け取れるように。
  • ついでにBMIの計算も関数化しておく。分数→小数は関数floatでできる
  • let使うときはprognは要らなかった。

これだけすればメインの関数は1-1とほとんど変わらなくなる。

(defun bmi-1-2 ()
  (let ((height) (weight) (bmi))
    (setq height (ask "身長は?"))
    (setq weight (ask "体重は?"))
    (setq bmi (calc-bmi height weight))
    (format t "あなたのBMIは~Aです~%" bmi)
    (format t "あなたは~A体型です~%" (cond
                                       ((< bmi 18.5) "痩せ")
                                       ((<= 25 bmi) "肥満")
                                       (t "普通")))))

まとめ

  • 割と手続きっぽい感じ?
  • 関数を引数として渡す時には#'をつける。F#とかJavaScriptとかは要らなかったのでちょっと面倒
  • マクロ使ってみたい

*1:例外処理の方法も調べておく