孤独にそっくり

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

実践Common Lispを読む 第10章〜第11章

Practical Common Lisp
原書はネットで読めるんですね。英語の必要性を感じるけど勉強したくない…

こんにちは。
第10章と第11章を読みました。
読書が楽しくて(というか、現実逃避が)一日中家でごろごろしながら読書していたら、なかなかこっちが進みません。
内田百閒の『東京日記』を読みましたが、のらりくらりとした文章でありながら妙にぞわぞわする話ばかりでした。ただどれも同じような雰囲気なので途中から少し退屈しました。
同時に高木貞治の『数学小景』を読んでいたところ、簡単な数学の遊戯の本かと思いきや、内容が普通に難しくてびっくりしました。
高木貞治のユーモアあふれる文章は好きです。もうちょっとで読み終わります。

読みながらとってるメモを貼り付けているだけなので、徐々に適当になってきましたが、今回はリスト以外のデータ構造とシーケンス的な話です。

第10章 数、文字、そして文字列

*数値リテラル

Lispは読取器と評価器に分かれている。読取器はテキストをLispオブジェクトにするまでを担当し、評価器はその受け取ったオブジェクトを扱える。
例えば10は10や20/2や#xAとかけるが、全て同じオブジェクトに変換する。

CL-USER> 10
10 
CL-USER> 20/2
10
CL-USER> #xA
10

いろいろなシンタックスがあるよ

*基本的な数学

四則演算は+,-,*,/
/は逆数を返す
/は結果を切り捨てない
数字を丸めるにはFLOOR、CEILING、TRUNCATE、ROUNDがある
MODは割り算の法、REMは剰余を返す

;1+、1-は新しい値を返す関数、incfやdecfは値を変更する
(incf x) = (setf x (1+ x)) = (setf x (+ x 1))
(incf x 10) = (setf x (+ x 10)
*数値比較

関数=は型の違いを無視して値を比べる
同じ値のときTを返す
/=はすべての引数が異なるときにのみ真を返す

(/= 1 2) -> T
(/= 1 2 3 1) -> NIL
(/= 1 2 3 1.0) -> NIL

<、>、<=、>=は複数の値も取れるが
その際には、それぞれの引数を左から右に向かって比較する

(<= 2 3 3 4) -> T
(<= 2 3 4 3) -> NIL

最小最大はMIN,MAX
他に便利な関数は
ZEROP、MINUSP、PLUSPは単一の実数が0に等しいか小さいか大きいか
EVENPとODDPは整数の引数が偶数か奇数か

*高度な数学

Lisp
対数 LOG
指数 EXP EXPT
三角関数 SIN COS TAN
逆関数 ASIN ACOS ATAN
双曲線関数 SINH COSH TANH
逆関数 ASINH ACOSH ATANH
などもある

*文字比較

大文字小文字を区別する場合には、数の比較関数の前にCHARをつければいい
例えばCHAR=のように。大文字小文字を区別しない場合には、CHAR-EQUALやCHAR-LESSPなどある。詳しくはCommon Lisp Hyper Specでcharがつくところを見ればいい

*文字列比較

CHARと同じくCommon Lisp Hyper SpecのSTRINGのところを見るべし。
ただし、文字列の場合はキーワード引数をとるから2つの文字列の比較しかできない。
それに、文字列が異なった場合は、文字列の違いが見つかった最初のインデクスを返す

(string/= "lisp" "lissome") -> 3

第11章 コレクション

Lispはリストが柔軟で何でも使えるからそこに目が行きがちだが、他のデータ構造もあるから使えばいいと思うよ。ここではベクタとハッシュテーブルを見る。

*ベクタ

ベクタは2つある。固定サイズのベクタはJavaにおける配列っぽい。可変サイズのベクタは、PerlRubyの配列、やPythonのリスト、JavaArrayListっぽい。

固定サイズのベクタはVECTOR関数で作れる。

CL-USER> (vector 1 2)
#(1 2) 
;#は印字器や読取器が使うベクタのためのリテラル表記

MAKE-ARRAYは汎用の関数。固定、可変の他、任意次元の配列が作れる。
MAKE-ARRAYの引数にはリストの代わりに数を指定することもできる
:initial-element で初期値を指定できる

CL-USER> (make-array 5 :initial-element nil)
#(NIL NIL NIL NIL NIL) 

可変なベクタの場合には、次に要素を追加した時に埋める(fill)位置、フィルポイントを指定する。

(make-array 5 :fill-pointer 0) -> #()

フィルポイントが0のため、空に見える

サイズが変更できるベクタの末尾に要素を追加するには、VECTOR-PUSHを使う。これは、フィルポイントが示す場所に要素を追加し、フィルポインタの値を一つ増やす。
VECTOR-POPは、最後に追加された要素を返し、フィルポインタの値を一つ減らす。

ただし、フィルポインタだけでは、可変なベクタではない。
別のキーワード引数:adjustableを追加する必要がある

(make-array 5 :fill-pointer 0 :adjustable t)

だいたいは一緒に宣言される。
特殊ベクタってのもあるけど割愛。

*シーケンスとしてのベクタ

ベクタとリストはどちらもシーケンスという抽象的な型の別々のサブタイプ。
だからシーケンス関数はどちらでも使える。
最も基本的なものはシーケンスの長さを返すLENGTHと、整数のインデックスを使ってシーケンス中の要素にアクセスするELT(elementの略)。ELTは要素がないとエラーを返す。

*シーケンス反復関数

わざわざループを書かなくてもフィルターしてくれるシーケンス関数がある
出現回数を返すCOUNT、アイテムかNILを返すFIND、場所かNILを返すPOSITION、アイテムを削除したシーケンスを返すREMOVE、値を置き換えるSUBSTITUTE
これらの関数の振る舞いはキーワード引数を使うことでさまざまに変更できる。

(count "foo" #("foo" "bar" "baz") :test #'string=) -> 1
;キーワード引数によっては返り値が変わることもある
(find 'a #((a 10) (b 20) (a 30) (b 40)) :key #'first) -> (A 10)
(find 'a #((a 10) (b 20) (a 30) (b 40)) :key #'first :form-end t) -> (A 30)

キーワード引数はいろいろある
:test 比較
:key
:start 開始位置
:end 終了位置
:form-end 真ならば末尾を開始位置にして逆順にトラバース
:count removeもしくはsubstituteする要素の数を示す

高階関数

ここまで見てきた関数には、亜種が2種類用意されている。
引数を渡した関数が真を返す場合に操作をする-IFとその逆の-IF-NOT

(count-if-not #'evenp #(1 2 3 4 5)) -> 3
(remove-if-not #'(lambda (x) (char= (elt x 0) #\f)) #("foo" "bar" "baz" "foom")) -> #("foo" "foom")

ここで見たように、remove-if-notは便利だよ

  • IFや-IF-NOTでも、:test以外のキーワード引数も取れる

removeはremove-deplicatesっていうのもある

*シーケンス全体の操作

シーケンス全体を一度に操作するものにはCOPY-SEQやREVERSEやCONCATENATEなどがある

*ソートとマージ

シーケンスのソートには、SORTとSTABLE-SORTがあり、STABLE-SORTは述語によって等価だと判断された要素は順序が入れ替わらないことが保証される。だが、どちらも破壊的関数の代表。けれど、もし使うなら、次のように書くこと。

(setf mysequence (sort mysequence #'string<))
;下ではダメ
(sort mysequence #'string<))

SORTもSTABLE-SORTもキーワード引数:keyを取る。

MERGE関数は2つのシーケンスと述語を取り、2つのシーケンスを述語に照らし合わせてマージして生成したシーケンスを返す。:keyも使える。

(merge 'list #(1 2 3) #(2 4 6) #'<) -> (1 2 3 4 5 6)
*シーケンス述語

真偽を評価するのに便利な関数がEVERY、SOME、NOTANY、NOTEVERYの4つ。
EVERYは述語が失敗すると同時にNILを返す。常に満たされる場合はTを返す。
SOMEは述語が最初にNIL以外の値を返したときに真を返し、述語を満たす要素がひとつもなかったらNILを返す。
NOTANYは述語が満たされると同時にNILを返し、最後まで満たされなかったらT。
NOTEVERYは述語が失敗すると同時に真を返し、常に満たされる場合はNILを返す。

*シーケンスマッピング関数

MAPはどんな種類のシーケンスを作るか指定する必要がある

(map 'vector #'* #(1 2 3 4 5) #(10 9 8 7 6)) -> #(10 18 24 28 30)

MAP-INTOは第1引数として渡したシーケンスに値を設定する以外はMAPと同じ

REDUCEは1つのシーケンスに対して動作する。はじめに2つの要素について引数を適用し、それからその結果と次の値に引数を適用する。

(reduce #'+ #(1 2 3 4 5 6 7 8 9 10)) -> 55

便利。
キーワード引数はいろいろ取れる。:initial-valueは固有の引数で、シーケンスの先頭よりも前にくる値を指定する。

*ハッシュテーブル

ハッシュテーブルが使える。
MAKE-HASH-TABLE
ハッシュテーブルにアクセスするにはGETHASH関数を使う
この関数はキーとハッシュテーブルを引数にとり、値があれば値を、なければNILを返す
値がNILのときは値があるのかないのか判別できなさそうだけど、「多値」を使うことで解決されている。GETHASHは実は2つ値を返している。ひとつめが値かNILで、ふたつめがキーがハッシュテーブルに出現するかの真偽値
ひとまずMULTIPLE-VALUE-BINDマクロを使うと見れる
テーブルからエントリを削除するにはREHASH、全て削除するにはCLRHASH

*ハッシュテーブルの反復

一番シンプルにはMAPHASH関数を使うこと

;10より小さいエントリを全て削除
(maphash #'(lambda (k v) (when (< (v 10) (remhash k *h*))) *h*)

LOOPでもできる

まとめ

へえ、って感じのお話しでした。シーケンス関数とキーワード引数のあたりは詳しく書かれていて初めて知ることが多々ありました。
それはともかく、エントリを書くときにはてな記法で毎回">|lisp| ||<"って書くのめんどくさいしどうにかしたいです。次回はリストです。