この文章は、 Lisp Advent Calendar 2014 の 12/11 担当分の記事として書かれました。
C系言語から Common Lisp に移行した時、「どうして Lisp はこんなに書き辛いんだ?」と思っていたことを記憶しています。
こう思った一番の要因は、 Lisp の一般的なプログラミングスタイルを承知していなかったことなのですが・・ その時代のことをなんとなく思い出して書いてみようと思います。
僕は、 C をやり、 C++ をやり、その後 Lisp に流れてきた人間でした。
そんなわけで、 C系言語で身につけたスタイルが染み付いた状態でコードを書いて遊んでいたのですが・・ どうしてこんなにネストが深くなるんだ!? と思っていたのです。
C++ を書いてたとき、こんなスタイルで書いてました:
int func (...) {
if (/* エラーを確認して */) {
return ERROR_1; // 無理ならさっさとやめる。
}
if (/* またエラーを確認して */) {
return ERROR_2; // 無理ならさっさとやめる。
}
// そろそろメインの処理
int xxx = ... ; // 確認した上で値を取る。
int yyy = xxx + 1; // なんか上のに依存した値も取ったり
hoge1(...); // 変数 zzz のために、いくつか前処理を呼んで・・
hoge2(...);
int zzz = hoge3(...); // 値を取る。
fuga1(...); // 変数 www のために、いくつか前処理を呼んで・・
fuga2(...);
int www = fuga3(...); // 値を取る。
return xxx + yyy + zzz + www; // そして何か返す。
}
このノリで Lisp を書くと・・
(defun func (...)
(if (some-condition-1) ; エラーを確認して
ERROR_1 ; 無理ならさっさとやめる
(if (some-condition-2) ; またエラーを確認して
ERROR_2 ; 無理ならさっさとやめる
;; そろそろメインの処理
(let ((xxx ...)) ; 確認した上で値を取る
(let ((yyy (+ 1 xxx))) ; なんか上のに依存した値も取ったり
(hoge1 ...) ; 変数 zzz のために、いくつか前処理を呼んで・・
(hoge2 ...)
(let ((zzz (hoge3 ...))) ; 値を取る。
(fuga1 ...) ; 変数 www のために、いくつか前処理を呼んで・・
(fuga2 ...)
(let ((www (fuga3 ...))) ; 時たま値を取る。
(+ xxx yyy zzz www)))))))) ; そして何か返す。
こんな、なんだか異常にネストが深いコードを書いておりました。当時は、
- 早く制御を戻そうとするとネストが深くなるとか、おかしいだろ!?
- 変数を使おうとするとネストが深くなるとか、おかしいだろ!?
とかなんとか思っていた気がします。
エラーをなぜか返り値で戻そうとしていて、それが妙な制御フローを招いています。 素直にエラーを報告しましよう:
(defun func (...)
(when (some-condition-1) ; エラーを確認して
(error ERROR_1)) ; 無理ならさっさとやめる
(when (some-condition-2) ; またエラーを確認して
(error ERROR_2)) ; 無理ならさっさとやめる
;; そろそろメインの処理
(let ((xxx ...)) ; 確認した上で値を取る
(let ((yyy (+ 1 xxx))) ; なんか上のに依存した値も取ったり
(hoge1 ...) ; 変数 zzz のために、いくつか前処理を呼んで・・
(hoge2 ...)
(let ((zzz (hoge3 ...))) ; 値を取る。
(fuga1 ...) ; 変数 www のために、いくつか前処理を呼んで・・
(fuga2 ...)
(let ((www (fuga3 ...))) ; 値を取る。
(+ xxx yyy zzz www)))))) ; そして何か返す。
let
の直下で let
するという、まさにその目的のために、 let*
があります。
素直に使いましょう:
(defun func (...)
(when (some-condition-1) ; エラーを確認して
(error ERROR_1)) ; 無理ならさっさとやめる
(when (some-condition-2) ; またエラーを確認して
(error ERROR_2)) ; 無理ならさっさとやめる
;; そろそろメインの処理
(let* ((xxx ...) ; 確認した上で値を取る
(yyy (+ 1 xxx))) ; なんか上のに依存した値も取ったり
(hoge1 ...) ; 変数 zzz のために、いくつか前処理を呼んで・・
(hoge2 ...)
(let ((zzz (hoge3 ...))) ; 値を取る。
(fuga1 ...) ; 変数 www のために、いくつか前処理を呼んで・・
(fuga2 ...)
(let ((www (fuga3 ...))) ; 値を取る。
(+ xxx yyy zzz www))))) ; そして何か返す。
上記のように書いていると、変数 www
を得るためには、 hoge1
, hoge2
, hoge3
の呼び出しが必須であるように見えます。
しかし実際には、直接に必要なのは変数 zzz
であり、それさえ手に入れば依存関係はない、なんてことがあります。
それなら、こう書けるでしょう。
(defun func (...)
(when (some-condition-1) ; エラーを確認して
(error ERROR_1)) ; 無理ならさっさとやめる
(when (some-condition-2) ; またエラーを確認して
(error ERROR_2)) ; 無理ならさっさとやめる
;; そろそろメインの処理
(let* ((xxx ...) ; 確認した上で値を取る
(yyy (+ 1 xxx)) ; なんか上のに依存した値も取ったり
(zzz (progn (hoge1 ...) ; 変数 zzz のために、いくつか前処理を呼んで・・
(hoge2 ...)
(hoge3 ...))); 値を取る。
(www (progn (fuga1 ...) ; 変数 www のために、いくつか前処理を呼んで・・
(fuga2 ...)
(fuga3 ...)))) ; 値を取る。
(+ xxx yyy zzz www))) ; そして何か返す。
C系言語の場合、変数の宣言だけではネストが深くならないということもあり、適当にだらだら変数を宣言しながら書いていっても、そんなに見苦しくならない気がします。 一方 Lisp の場合、そうしているとどんどんネストが深くなり、ゲロ以下のにおいがするコードになってしまいがちでした。
今から思うと、そういうのは適切な書き方を知らなかったからなんだなあ、と思います。 つまり・・
let*
や、もしくはwith-
系マクロなど、変数を導入するマクロに一通り目を通しておく。- Lisp 流のエラー処理 (condition) を知っておく。
loop
やformat
なども、伏魔殿っぽいなあといって忌避しない。
くらいかなあ、と思います。
全編を疑似コードで通すのもアレなので、ディスクの隅に眠っていた吐き気を催すコードをとりだして、あげつらってみようと思います。
(defun fizzbuzz1 (max)
(labels ((printer (x)
(cond ((= (mod x 15) 0) "FizzBuzz")
((= (mod x 3) 0) "Fizz")
((= (mod x 5) 0) "Buzz")
(t x))))
(dotimes (x (1+ max))
(print (printer x)))))
- この規模で
labels
いるか?flet
でいいし、そもそも分けるような規模じゃないだろ。 printer
って名前なのに print してないっていう詐欺名称。頭わいてる。
(defun fizzbuzz1 (max)
(dotimes (x (1+ max))
(print
(cond ((= (mod x 15) 0) "FizzBuzz")
((= (mod x 3) 0) "Fizz")
((= (mod x 5) 0) "Buzz")
(t x)))))
(defun prime? (n)
(labels ((worker (n i i-max)
(cond ((> i i-max) t)
((zerop (mod n i)) nil)
(t (worker n (+ i 2) i-max)))))
(cond ((<= n 1) nil)
((= n 2) t)
((zerop (mod n 2)) nil)
(t (worker n 3 (isqrt n))))))
なんで再帰で書きたかったの? ってのは置いておくとして
labels
のスコープは、もっと狭くていいでしょう? 先頭に置くと、cond
のどこからでも参照されるのかと思うが、実は一箇所だけ。- どうして変化させるつもりがない値を引数で渡してるのだろう
(defun prime? (n)
(cond ((<= n 1) nil)
((= n 2) t)
((zerop (mod n 2)) nil)
(t
(let ((i-max (isqrt n)))
(labels ((worker (i)
(cond ((> i i-max) t)
((zerop (mod n i)) nil)
(t (worker (+ i 2))))))
(worker 3))))))
もう loop
でいいんじゃないかな・・ let
も with
で内包できるし。
(defun prime? (n)
(cond ((<= n 1) nil)
((= n 2) t)
((zerop (mod n 2)) nil)
(t
(loop with i-max = (isqrt n)
for i from 3 to i-max by 2
never (zerop (mod n i))
finally (return t)))))
これはまあこれでいいんですが
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
int hello_tm(const char *msg) {
time_t now = time(NULL);
struct tm now_tm;
if(!localtime_r(&now, &now_tm))
return EXIT_FAILURE;
char now_str[32] = {0};
size_t now_str_len = strftime(now_str, sizeof(now_str),
"%F", &now_tm);
if(now_str_len == 0)
return EXIT_FAILURE;
printf("%*s: %s\n", (int)now_str_len, now_str, msg);
return EXIT_SUCCESS;
}
int main() {
return hello_tm("Hello, World!");
}
Common Lisp には locale がなく、 strftime
がないので、適当に手書きしてます。
(defun hello-tm (msg)
(let ((now (get-universal-time)))
(multiple-value-bind (_ __ ___ d m y)
(decode-universal-time now)
(declare (ignore _ __ ___))
(let ((str (with-output-to-string (stream)
(format stream "~D-~D-~D" y m d))))
(format t "~A: ~A~%" str msg)))))
get-universal-time
とdecode-universal-time
の組み合わせは、get-decoded-time
でよい。- いちいち
format
を二回呼ばなくても、一回の呼び出しにまとめられる。
(defun hello-tm-2 (msg)
(multiple-value-bind (_ __ ___ d m y)
(get-decoded-time)
(declare (ignore _ __ ___))
(format t "~D-~D-~D: ~A~%" y m d msg)))
ご存知の通り、 Common Lisp の format
には ~//
ディレクティブというものがあり、 format
から任意の関数を呼んで仕事をさせることが出来る。
Allegro CL には、 この形式で C の strftime
を使うための locale-format-time という関数があるため、単一の format
で達成することが出来るのだ!
(defun hello-tm-acl (msg)
(format t "~,V:@/locale-format-time/: ~A~%"
"%F"
(get-universal-time)
msg))