孤独にそっくり

開いている窓の前で立ち止まるな

実践Common Lispを読む 第21章〜第22章

Lisp 3EK
Lispの壁画

こんにちは。
今回でCommon Lispの基本的な知識の部分は終わりになります。

第21章 大規模開発に向けて:パッケージとシンボル

さしあたってパッケージは文字列とシンボルとを対応付ける表と考えればいい。
以下、詳しすぎる説明は割愛

e-mailのメッセージを検索データベースに挿入するプログラムの例

;;COM.GIGAMONKEYS.EMAIL-DBという名前のパッケージを定義
(defpackage :com.gigamonkeys.email-db
  (:use :common-lisp))
;;パッケージ内のコードを読むには、IN-PACKAGEマクロを使ってレントパッケージにする必要がある
(in-package :com.gigamonkeys.email-db)
;;パッケージを切り替え
;;CL-USER、EMAIL-DBのどちらにもHELLOW-WORLD関数があった場合でも、大丈夫
CL-USER> (in-package :com.gigamonkeys.email-db)
#<PACKAGE "COM.GIGAMONKEYS.EMAIL-DB">
EMAIL-DB> 
;;もし、他のCOM.GIGAMONKEYS.TEXT-DBを利用する場合は、次のようにする。
(defpackage :com.gigamonkeys.email-db
  (:use :common-lisp :com.gigamonkeys.text-db))
;;サードパーティのライブラリCOM.ACME.EMAILから特定の関数PARSE-EMAIL-ADRESSを利用する場合(他の関数でシンボル名がかぶってしまうとか)は以下
(defpackage :com.gigamonkeys.email-db
  (:use :common-lisp :com.gigamonkeys.text-db)
  (:import-from :com.acme.email :parse-email-adress)
;;SHADOW節やSHADOWING-IMPORT-FROMでも特定の関数を隠せる

慣れれば大したこと無いけどライブラリ関連は新人Lisperには結構めんどくさいかも

第22章 黒帯のためのLOOP

LOOPは複雑で面倒だけど便利

*LOOPのパーツ

LOOPは変数の更新やループしている間に見える値のcollect、count、sum、minimize、maximizeやLisp式の実行やループ終了の決定や条件付きの実行ができる
加えて、ループ内でのローカル変数の生成、ループ前後に実行する任意のLisp式の実行ができる

主要なループキーワードはfor、collect、sum、count、do、finallyなど

*反復の制御

ループはforかas(ループは英語っぽいシンタックスで書くために同義語が用意されている)で始まり、変数の名前が続く(これをfor節という)。反復の指定には、区間や数値の範囲やリストの個々の要素やコンスセルやベクタの要素などなど使える
ループに複数のfor節がある場合は、どれか1つが終了条件を満たせば終了する

;;最大10回繰り返すが、リストの要素が10個未満の場合はそこで止まる
(loop
     for item in list
     for i from 1 to 10
     do (something))
*数えるループ

for節を構成するうち、どこから(from where)、どこまで(to where)、いくつずつ(by how much)が前置詞句と呼ばれる。
それぞれ
from where : from、downfrom、upfrom
to where : to、upto、below、downto、above
減少方向に新させる場合は初期値が必要
LOOPはマクロであり、コンパイル時に実行されるので、変数が更新される増減の方向は前置詞で決定できなければならない。

指定した回数を繰り返す場合はrepeat

*コレクションやパッケージのループ

inとon
inはリストの全要素
onはコンスセルにわたって更新

(loop for i in (list 1 2 3 4) collect i) -> (1 2 3 4)

(loop for x on (list 1 2 3) collect x) -> ((1 2 3) (2 3) (3))

ハッシュテーブルやパッケージはもっと複雑

(loop for var being the things in hash-or-package ...)
*=とthenによる反復
;;基本フォーム
(loop for var = initial-value-form [then step-form] ...)
;;例
(loop repeat 5
     for x = 0 then y
     for y = 1 then (+ x y)
     collect y) -> (1 2 4 8 16)
;;新しい値が与えれらる前にstep-formを全て評価するにはandを使う
(loop repeat 5
     for x = 0 then y
     and y = 1 then (+ x y)
     collect y) -> (1 2 4 8 16)
*ローカル変数

with節を使う

with var [ = value-form]
*値の累積

累積節は動詞verbで始める

verb form [into var]

ループのたびにformを評価し、その値をverbで累積していく
動詞は、collect、append、nconc、count、sum、maximize、minimize(現在分詞形~ingも同じ意味)
collectは出てきた順のリストをつくる
appnedやconcは値にリストをとる

;;乱数生成
(defparameter *random* (loop repeat 100 collect (random 10000)))
;;様々なリストを返す
(loop for i in *random*
     counting (evenp i) into evens
     counting (oddp i) into odds
     summing i into total
     maximizing i into max
     minimizing i into min
     finally (return (list min max total evens odds)))
;;やってみた
(5 9987 483144 50 50)
CL-USER> *random*
(7926 8057 5 3002 2347 9765 3354 5860 6906 5281 5393 1203 311 9386 9810 5144
 7995 3121 9390 2055 6505 5293 2987 2440 8012 3258 9871 896 2501 5158 5723 450
 3789 2286 9780 9234 2393 9060 5476 8393 1497 8649 154 2529 7918 129 3368 3302
 771 4267 780 1289 8712 6905 1788 4403 1279 9576 6768 5209 7953 182 8208 8645
 4403 2384 1953 5624 1236 7016 5827 5244 2494 7971 9891 833 3215 8033 5034 4404
 9262 4379 8650 514 3972 877 106 8970 5284 1354 9987 3290 9767 2361 3548 4321
 2058 2077 6789 5919)
*無条件実行

ループ内で任意のコードを実行するにはdo
もうひとつ直ちに実行されるのはreturn

(loop for i from 1 to 10 do (print i))

;;return節はLOOP式から戻るが
;;LispのオペレータであるRETURNやRETURN-FROMはブロックから脱出することもできる
(block outer
  (loop for i from 0 to return 100);100 returned from LOOP
  (print "This wil print")
 200) -> 200
(block outer
  (loop for i from 0 to return 100);100 returned from BLOCK
  (print "This won't print") 
 200) -> 100
*条件実行

IFやWHENも使えるが、LOOPには3種類の条件構文が用意されている

;;基本フォーム
conditional test-form loop-clause

conditionalはifとwhenとunless
testは何でも
loop-clauseは累積節(countやcollect)や無条件実行節やその他条件実行節が使える
さらに、ループ節内のtest-formのあとで、test-formに返された値を参照する変数itが使える
andやelseやendも使える

;;わざとらしい例
(loop for i from 1 to 100
     if (evenp i)
      minimize i into min-even and
      maximize i into max-even and 
      unless (zerop (mod i 4))
      sum i into even-not-fours-total
     end
     and sum i into even-total
   else
     minimize i into mini-odd and
     maximize i into max-odd and
     when (zerop (mo i 5))
      sum i into fives-total
     end
     and sum i into odd-total
   do (update-analysis min-even
		       max-even
		       min-odd
		       max-odd
		       even-total
		       odd-total
		       fives-total
		       even-not-fours-total))

何が何やら

*セットアップと後始末

LOOPにはinitiallyとfinnallyという2つのキーワードがあるため、ループの本体部の外で動くコードが導入できるようになっている。
initiallyに続く前処理部はループの前に一度だけ必ず実行される
finallyは以下の場合をのぞいて実行される

  • return節が実行されるとき
  • RETURNやRETURN-FROM
  • always、never、thereis節でループが終了するとき
*終了条件のテスト

終了条件節は、while、until、always、never、thereisがある。

;;基本フォーム
loop-keyword test-form

whileとuntilはループ本体を飛ばして後処理部に移す。
whileはtest-formが最初に失敗した場合
untilは最初に真になったとき
残りの3つは有無をいわさずループを終了させる。
これらの終了条件節はループのデフォルト値を提供する。
そのため、into副説を持たない累積節とは組み合わせられない。
alwaysとneverはブール値(T,NIL)を返す
thereisはtest-formの返り値かNILを返す

*LOOPのまとめ

LOOPは以下のルールに従えば自由に書ける

  • named節を書く場合は先頭の節にする(こんなのあったっけ?)
  • initially節、with節、for節、repeat節は全てnamed節の後に書く
  • 本体となる節(条件実行節、無条件実行節、累積節、終了条件節)はその後
  • 最後にfinally節

LOOPマクロの展開順序

  • with節やfor節で宣言されたローカルな変数と、累積節で暗に作られた変数を初期化する。ループ内に出てきた順番で初期値のフォームが評価
  • initially節をループの順番で実行
  • ループ本体の実行
  • finally節をループの順番で実行

まとめ

今回で、基本的な説明は全て終わりらしいです。次回からは全て実践の章になります。
パッケージの使い方とかそこら辺はとても実用的で、LOOPに関してもかなり詳しく説明が載っていてよかったです。やっと実践の章に入りますが、一旦図書館に返却しなくてはいけないので、最後まで読み終えるのは、だいぶ先になりそうです。
次の実践はポール・グレアムベイジアンフィルタを用いたスパムフィルタらしいですよ。ザ・実践ですね