Displaying posts tagged with

“『実践Common Lisp』”

『実践Common Lisp』23章

293ページ脚注に載っているURLはリンク切れになっている。SpamAssassinが提供するSPAMとHAMのコーパスはここ

ただし本に載っている通りのコードでは start-of-file でエラーが出た。環境はSBCL@MacOS X。コーパスの中にiso-8859-1でエンコードされたファイルが混じっているのが原因らしい。with-open-file に :external-format を指定したら動作した。

(defun start-of-file (file max-chars)
  (with-open-file (in file :external-format :latin1)
    (let* ((length (min (file-length in) max-chars))
	   (text (make-string length))
	   (read (read-sequence text in)))
      (if (< read length)
	  (subseq text 0 read)
	  text))))

SBCLで使用可能な :exteral-format の一覧を探しているのだが、マニュアルのどこに書いてあるのか分からない…。

『実践Common Lisp』23.2 リスト指示子?

『実践Common Lisp』286ページの脚注8、リスト指示子についての説明(翻訳)が完全に混乱していて、全く理解できない。

HyperSpecを参照しながら、自分なりに読み解いてみた。

1.4.1.5 Designators

本書で「指示子」と訳されている designator とは、別のオブジェクトを意味する/指し示すオブジェクトである。例えば format 関数の最初のパラメータに t を与えると *terminal-io* を与えたのと同じことになるが、これは t というシンボル・オブジェクトが *terminal-io* に保持されるストリーム・オブジェクトを意味する/指し示すからだ、と言うことができる。

あるオブジェクトがどのオブジェクトを指し示すかというルールは、指示子の型によって決められている。だから指示子は普通、型名を伴って

  • <<type>> designator : 〜型の指示子
  • designator for a <<type>> : 〜型に対する指示子

とかいう呼び方をする(訳語は私による)。例えばシンボル t が *terminal-io* を指し示すというルールは stream desinator によって決められている。

……

『実践Common Lisp』286ページ脚注に戻ると、ecase のキーは “designator for a list of objects” と定義されている。そこでHyperSpecの用語集で list designator のところを読むと、次のようなルールが書いてある。

  • nilではないアトム -> そのアトムを唯一の要素として持つリストを指し示す
  • 普通のリスト -> そのリスト自身を指し示す

つまり指定できるのは「nilではないアトム」か「リスト」のどちらかで、どちらを指定したとしてもリストと見なされる。

よって ecase の動作もキーがリストであることを前提として説明される。

If the test-key is the same as any key for that clause, the forms in that clause are evaluated as an implicit progn, and the values it returns are returned as the value of the case, ccase, or ecase form.

(そもそもこの短い脚注で指定子の概念にまで言及したせいで、翻訳も混乱してしまったのではないかと思う。『ecaseのキーにはリストも指定できる。リストを指定した場合はいずれかの要素が一致する場合にマッチしたと見なされる』くらいの言い方で良かったのではなかろうか…)

『実践Common Lisp』20.6 EVAL-WHEN

「20.6 EVAL-WHEN」に書いてある内容がどう考えてもおかしくて、これは翻訳のミスじゃないかと思って調べてみたら、やはり正誤表に載っていた。とはいえ多分、そもそも原文も分かりにくいのだろうと思う。HyperSpecの説明の方がずっと分かりやすい。

:compile-toplevel と :load-toplevel が意味を持つのは、次の2つの条件が揃ったときだけだ。

  1. COMPILE-FILE でlispファイルをコンパイルするとき
  2. EVAL-WHEN がトップレベルフォームとして現れる場合

:comile-toplevel が指定されていれば、コンパイル時に評価される。:load-toplevel が指定されていれば、faslファイルを LOAD したときに評価される。

上の2条件が揃わない場合は、「:execute が指定されていれば評価される」「:execute が指定されていないと評価されない」という、ごく単純な動作になる。

HyperSpecに載っている例よりも、さらに直接的な例を考えてみた。

(eval-when (:execute)
  (defun foo ()
    "foo-execute"))
 
(eval-when (:compile-toplevel)
  (defun foo ()
    "foo-compile-toplevel"))
 
(eval-when (:load-toplevel)
  (defun foo ()
    "foo-load-toplevel"))
 
(defmacro foo-macro()
  (foo))
 
(defun main()
  (format t "~a~%" (foo))
  (format t "~a~%" (foo-macro)))

LOAD でlispファイルを読み込めば、

foo-execute
foo-execute

と表示される。COMPILE-FILEでコンパイルしたfaslファイルを読み込めば、

foo-load-toplevel
foo-compile-toplevel

と表示される。

Common LispでFizz-Buzz問題・コンディションで

コンディションの練習のために、Fizz-Buzz問題をコンディションを使って解いてみた。「15で割り切れる」「3で割り切れる」「5で割り切れる」といった事態をそれぞれエラーと見なし、別の値を使うという選択肢(use-value)を用意した上で、loop時にFizz,Buzz,FizzBuzzという文字列を割り当てている。

(define-condition multiple-of-15 (error) ())
(define-condition multiple-of-3  (error) ())
(define-condition multiple-of-5  (error) ())
 
(defun fizz-buzz-cond (n)
  (restart-case
      (cond ((zerop (mod n 15)) (error 'multiple-of-15))
	    ((zerop (mod n 3))  (error 'multiple-of-3))
	    ((zerop (mod n 5))  (error 'multiple-of-5))
	    (t n))
    (use-value (value) value)))
 
(defun use-value-fn (value)
  #'(lambda (c)
      (declare (ignore c))
      (invoke-restart 'use-value value)))
 
(defun run-fizz-buzz ()
  (handler-bind ((multiple-of-15 (use-value-fn "FizzBuzz"))
		 (multiple-of-3  (use-value-fn "Fizz"))
		 (multiple-of-5  (use-value-fn "Buzz")))
    (loop for n from 1 to 100 do (format t "~a~%" (fizz-buzz-cond n)))))

Common LispでFizz-Buzz問題・総称関数を使って

総称関数の練習のために、Fizz-Buzz問題を総称関数を使って解いてみた。独自のメソッド結合まで使った、無駄に大掛かりな物になっている。

(defun strcat (&rest strings)
  (apply 'concatenate `(string ,@strings)))
 
(define-method-combination strcat :identity-with-one-argument t)
 
(defgeneric fizz-buzz-method (mod3 mod5)
  (:documentation "mod3が0ならFizz, mod5が0ならBuzz")
  (:method-combination strcat))
 
(defmethod fizz-buzz-method strcat ((mod3 (eql 0)) mod5)
  "Fizz")
 
(defmethod fizz-buzz-method strcat (mod3 (mod5 (eql 0)))
  "Buzz")
 
(defmethod fizz-buzz-method strcat (mod3 mod5)
  "")
 
(defun empty-p (seq)
  (if (= 0 (length seq)) nil seq))
 
(defun fizz-buzz (n)
  (format t "~a~%" (or (empty-p (fizz-buzz-method (mod n 3) (mod n 5))) n)))

総称関数の使い勝手はHaskellやErlangのパターンマッチと似たところがあるが、マッチするメソッドが一度に全部手に入り、それらを結合できるところが決定的に違う。

Common LispでFizz-Buzz問題・さらに短く

『実践Common Lisp』18章を読んで、さらに別の書き方を思いついた。format関数の条件制御構文を使用する。

(defun fizz-buzz-format (n)
  (let ((s (format nil "~[Fizz~;~]~[Buzz~;~]" (mod n 3) (mod n 5))))
    (format t "~a~%" (if (string= "" s) n s))))

2つのformat関数を1つにまとめるのは…無理かな?

追記:思いついた

(defun fizz-buzz-format2 (n)
  (format t "~[Fizz~;~]~[Buzz~;~]~@*~[~:;~[~:;~a~]~]~%" (mod n 3) (mod n 5) n))

わずか一行に収まるとは。

Common Lisp で Fizz-Buzz問題

ベタに書けばこんな感じか。

(defun fizz-buzz-normal (num)
  (format t "~a~%" (cond
                     ((zerop (mod num 15)) "FizzBuzz")
                     ((zerop (mod num 3)) "Fizz")
                     ((zerop (mod num 5)) "Buzz")
                     (t num))))

with-output-to-string を使うとよりエレガント、かも知れない。

(defun fizz-buzz (num)
  (let ((r (with-output-to-string (s)
	       (when (zerop (mod num 3)) (format s "Fizz"))
	       (when (zerop (mod num 5)) (format s "Buzz")))))
    (format t "~a~%" (if (string= "" r) num r))))

一般化バージョン。

(defun gen-fizz-buzz (rules)
  (lambda (num)
    (let ((r (with-output-to-string (s)
		 (loop for (b w) in rules do
		      (when (zerop (mod num b)) (format s "~a" w))))))
      (format t "~a~%" (if (string= "" r) num r)))))

実行。

(loop for n from 1 to 100 do (fizz-buzz n))
 
;; 3,5,7の倍数の時、それぞれFizz,Buzz,Kozz
(let ((fizz-buzz-kozz (gen-fizz-buzz '((3 "Fizz") (5 "Buzz") (7 "Kozz")))))
    (loop for n from 1 to 100 do (funcall fizz-buzz-kozz n)))

SUBST-IFの動作に注意

『実践Common Lisp』13.1のSUBST-IFについての説明は間違っている。

SUBST-IFは、古いアイテムを1つ受け取る代わりに、1引数関数を受け取る。この関数は木に含まれる各アトミックな値に対して呼び出され、その結果が真になった箇所をすべて新しい値で置き換えた新しい木を返す。

「各アトミックな値に対して呼び出され」とあるが、実際にはコンスセルも含む(木自体も含む)全ての要素に対して呼び出される。SUBST-IF-NOTなども同様。

(subst-if 10 #'consp '(1 3 2 (1 2) (3 4) (5 6)))
;; => 10
 
(subst-if 10 #'atom '(1 3 2 (1 2) (3 4) (5 6)))
;; => (10 10 10 (10 10 . 10) (10 10 . 10) (10 10 . 10) . 10)
 
(subst-if 10 #'evenp '(1 3 2 (1 2) (3 4) (5 6)))
;; => エラー
;; The value (1 3 2 (1 2) (3 4) (5 6)) is not of type INTEGER.
 
(subst-if 10 #'(lambda (x) (and (integerp x) (evenp x)))
	  '(1 3 2 (1 2) (3 4) (5 6)))
;; => (1 3 10 (1 10) (3 10) (5 10))

そもそもアトミックな値も定義上は木である。

(subst-if 10 #'evenp 2)
;; => 10

Common Lispマージソート

組み込みの merge を使ってマージソートを実装してみた。

(defun merge-sort (result-type seq pred &key key)
  (let* ((len (length seq))
	 (mid (floor (/ len 2))))
    (if (> 2 len)
	seq
	(merge result-type
	       (merge-sort result-type (subseq seq 0 mid) pred :key key)
	       (merge-sort result-type (subseq seq mid) pred :key key)
	       pred
	       :key key))))

動作確認

(merge-sort 'list '("Wii" "DS" "PSP" "PS3" "Xbox360") #'string<)
;; => ("DS" "PS3" "PSP" "Wii" "Xbox360")
 
(merge-sort 'list '("Wii" "DS" "PSP" "PS3" "Xbox360") #'char<
              :key #'(lambda (x) (elt x 0)))
;; => ("DS" "PSP" "PS3" "Wii" "Xbox360")

『実践Common Lisp』読みかけの感想

11章途中まで読んだ。

他言語のプログラマに向けたLisp本。リストがどうしたコンスセルがどうしたという定番の説明をすっ飛ばして、とにかくまずはCommon Lispの凄さを見ろと言わんばかりに、最短距離でマクロの実演に突き進む。その姿勢は正しいと思う。ある程度経験のあるプログラマならリストやコンスセルの基礎くらいは知っているものだし、その上で「Lispって、結局何がどう凄いのかよくワカンネ」という人がこの本のターゲットだろう。

実際途中まで読んだだけでも、マクロの凄さはひしひしと伝わってくる。他言語での経験を振り返って、果たしてプログラムを書くということはどういうことだっただろうかと、改めて考え直してしまうくらい示唆に富んだ内容を含んでいる……。

ところでマクロのイディオムとして、次のように処理の”本体”となるコードを埋め込む形が多用されている。

(defmacro foo (a b &body body)
    `(bar (baz ,@body))

これはRubyのブロックによく似ている。一般にRubyのブロックは無名関数に対するシンタックスシュガーと見なされることが多いのだが、むしろLispのマクロを意識した部分も大きいのかも知れない。

…検索してみるとやはりLispのマクロとRubyのブロックを比較する視点は存在するようだ。Ruby作者のまつもとさん自身も取り上げている: