common lispプログラミング

Common lispの勉強をしていて出会ったことを書き連ねていく.

コーディングスタイル

  • 詳細を知りたい場合はGoogle Common lisp Coding規約を読む.
  • ファイル名はhoge-foo.lispという感じでハイフンでつなぐ。
  • グローバル変数は"hoge"
  • 定数は+hoge+
  • 大文字と小文字の違いはない。プログラムは小文字で書くことがほとんど。
  • 複数行のコメントは#| ... |#.

シンボルとキーワード

  • アトムはシンボルとシンボル以外に分類される。
    • シンボル以外は評価するとそれ自身を返す。例えば数値10とか文字列"hoge"とか。
    • シンボルは評価されると,それが指すものを返す。例えば,x=3のときにxを評価すると3が返る。リテラルはシンボルって考えれば良いのか?
  • +, sin, xなどはシンボルである。シンボルは評価するとシンボルが指すものに評価される。シンボル自身が欲しい場合はクォートする.
  • キーワードとは,評価すると自分自身になる特集なシンボル。
  • リーダは:で始まるシンボルを見ると,シンボルを登録する際にそのシンボルが指すものとして,そのシンボル自身を登録する。

format

  • ~がCで言う%(指示詞)
  • ~%は改行。~5%は5行改行。
  • ~Dは整数(大文字Dでも小文字dでも変わらない)
  • ~Aは文字列でも数値でも良きにやる.
  • 指示子には個数を設定できる.~10tは10カラム分のタブ.
  • ~{...~}は1つのリストを消費して,リスト内の各要素を~{...~}の中の指示子で消費していく.
  • print-prettyで改行とかを良きにやってくれる.
;; ~{...~}の例
(format t "~{~d ~d ~d~} ~a" '(1 2 3) :hoge)

print/princ/prin1/pprint

  • prin1 : readし易い形。
  • print : 一度改行を出力してからprint1。
  • princ : 人向け(エスケープ文字出さないとか)
  • pprint: printを読みやすくしたもの(連続スペースを1つに変換)

print1-to-string/write-to-string

  • xxx-to-stringはストリームじゃなくて,文字列として出力する。
  • writeはprin1よりも原始的で,print1はwriteの一部のオプションが設定されたもの。

破壊的代入

  • INCF, DECF, PUSH, POPなどは破壊的な代入操作をする.モディファイアマクロと呼ぶらしい.
  • 便利なマクロとして,x,yの値をswapするマクロ:ROTATEFがある.PUSH,POPも破壊的.
(defvar *x* 1)
(defvar *y* 2)
(incf *x*)        ; (setf *x* (+ 1 *x*))
(decf *x*)        ; (setf *x* (- 1 *x*))
(incf *x* 10)     ; (setf *x* (+ 10 *x*))
(rotatef *x* *y*) ; swap(x,y)

defunの変数は値渡し

  • defunで渡される引数は値渡し(ポインタじゃなくて,参照している値が渡される).
  • だから,当然だけど変数を引数で渡して,defunの中でその変数を更新するようなことは間違い.
  • ポインタ(consセル)を指している変数を渡すと,それはそのポインタのアドレスを渡していることになるから,その先の変数を変更できる.
;; 間違い
(defparameter *x* 0)
(defun hoge (x))
   (setf x 1))
(hoge *x*)        ; *x*は更新されない
(setf *x* (hoge)) ; 更新したければ返り値を使う

(defparameter *lst* '(1 2 3 4 5))
(defun foo (lst)
   (setf (car lst) 0))
(foo lst)         ; (0 2 3 4 5) 更新される

リスト処理

  • consやappendは元のリストは破壊しないので注意.
  • リストに要素を追加したい場合は,明示的にconsしたものをsetfするか,PUSHを使う(取り出すときはPOP) .
  • sortすると元の配列が壊される.しかもsortされる訳でもない?サイズが変わったりもしてる.なぜ?
  • リストをシフトしたいときはalexandriaのrotate.引数で右も左もシフトできる.
(setf lst (loop for x from 0 to 100 collect x))
(sort lst #'>)
lst ; ソートされたものの一部が入っている?

Array

  • 可変長配列(:adjustableを指定すれば)
  • make-arrayで作って,arefで参照して,setf+arefで代入する。
;; 配列生成
(defparameter *arr* (make-array 3)) ; => #(0 0 0)
(aref *arr* 0) ; => 0

;; 初期値指定
(defparameter *arr* (make-array 3 :initial-element 5)) ; => #(5 5 5)

VectorとArray

  • VECTORは固定長,ARRAYは可変長であり,多次元も可能.
  • 可変長なARRAYを作りたい場合は,:fill-pointerと:adjustableを設定する.:fill-pointerは現在の末尾の要素番号を指す整数.可変長のARRAYに要素を追加する場合はvector-push-extendを使う(vector-pushじゃない)
  • VECTORにもmapを適用できる.(map 'vector #'func array)形式.
(defparameter *varr* (make-array 0 :fill-pointer 0 :adjustable t))
(vector-push-extend 0 *varr*)   ; *varr* = #(0)
(vector-push-extend 1 *varr*)   ; *varr* = #(1 0)
(vector-push-extend 2 *varr*)   ; *varr* = #(2 1 0)
(map 'vector #'print *varr*)

ハッシュ

  • 文字列をKeyにするときは,:test 'equalを指定する.デフォルトはeqlになってシンボルを想定している.
  • loopでループする時は独特なので覚えてしまう.
(defparameter *hash* (make-hash-table :test 'equal))
(setf (gethash "hoge" *hash*) 1)
(setf (gethash "hogehoge" *hash*) 2)
(setf (gethash "hogehogehoge" *hash*) 3)
(gethash "hogehoge" *hash*)
(loop for k being the hash-key in *hash* collect k)
(loop for v being the hash-value in *hash* collect v)

setfとsetqの違い

  • setqよりもsetfの方が賢い.
  • setqは第一引数を必ずQuateする.
  • setfは第一引数が必要なら評価して,不要なら評価しない.

eltとarefとnthの違い

  • eltはシーケンス全般に使えるが,nthはリスト,arefはアレイ(ベクター)のみに使える.文字列もベクタなのでarefが使える.
  • eltは汎用なので型チェックが動的に行われるが,nth/arefはそれが無いのでnth/arefの方が速い.
  • 速度差があると言われているけれど,sbclだと違いわからなかった.測定ミス???

defvarとdefparameterの違い

  • 使い方の違いとして、defvarはプログラムによる変数の操作、defparameterはプログラムに対する変数の操作。
  • defvarは既に定義済みの変数の場合には再定義しない。
  • defparameterは定義済みの有無に関わらず再定義する。
  • defparameterは初期値が必ず必要.初期値が無い場合は:unboundを与える.

nullとendpの違い

  • nullはnilの時にTで,それ以外は文字列でもアトムでもFNILを返す.
  • endpはリスト専用で,アトムや文字列を入れるとエラーを発生する.

funcallとapply

  • funcallは引数をリストにまとめて渡す、applyはリストを渡す。
(funcall #'+ 2 3 4)
(apply #'+ '(2 3 4)

リーダマクロ

  • (quote (1 2 3)) -> '(1 2 3)
  • (function hoge) -> #'hoge これは関数名から関数の本体を得る方法。つまり関数のシンボルhogeが指すものを得る。
  • "#+","#-"は処理系依存コードなどを書く目的で,#+は式が真のときに読み込まえて,#-は偽の時に読み込まれる。どんな処理系でも処理系のシンボルは定義されている。例えばSBCLなら:sbclが定義されている.これを使って,処理系によって分岐するコードは下記になる. 例) #+:sbcl (expr for sbcl case) #+:ccl (expr for clozure cl case) #-(or :sbcl :ccl) (expr unknown case)
  • Vector/StructはそれとReaderがわかるようにprintでもサフィックスがつく。printで出力して,それをreadで読みこめば,そのままベクタ,構造体として扱える.
    • vector: #(1,2,3)
    • struct: #S(struct-name (:slot-name ...)* )

functionが不要?

  • 下のコードが動くのはなんで?"#"は要らないの?
(defun square (x)
  (* x x))
(mapcar #'square '(1 2 3 4 5)) ; (1 4 9 16 25) 
(mapcar 'square '(1 2 3 4 5))  ; (1 4 9 16 25) なんで???

ループ

do系

  • doは複雑なように見えて実はCのforと大きくは変わらない.初期値;更新式;終了条件;ループ本体,って感じだ.
;; リストでのループ
(dolist (v '(1 2 3 4 5 6 7 8 9))
  (print v))
;; 指定回数のループ
(dotimes (i 10)
  (print i))
;; do
;; (do ((変数, 初期値, 更新式)*) 
;;     (終了判定式 返り値)
;;     (body*))

loopマクロ

;; loop in list
(loop for x in '(a b c d e)
     do (print x))
;; with-when
(loop for d in '(0 1 2 3 4 5 6 7 8 9)
   when (evenp d)
   do (print d))
;; with if
(loop for d in '(0 1 2 3 4 5 6 7 8 9)
   if (evenp d)
   do (print d))
;; with while
(loop for d in '(0 1 2 3 4 5 6 7 8 9)
   while (< d 5)
   do (print d))

;; Counting
(loop for x from 0 to 10 by 2
     do (print x))

;; repeat
(loop repeat 5
     do (print 'hoge))

;; assign and update
(loop for x1 = 0 then x2
     for x2 = 1 then (+ x1 x2)
     while (< x1 30)
     do (print x1))

;; iota
(loop for x from 0 to 10 collect x)
(setf lst '(1 2 3 4 5))
(loop
   for x in lst
   for y in (cdr lst)
     do (format t "x:~d y:~d~%" x y))

#'(lambda ...)の意味

  • lambda式は関数オブジェクトを返すから,#'は不要なんじゃないの?と思うけど,下のような例が見られる.
(mapcar #'(lambda (x) (* 2 x)) '(1 2 3 4 5))
(mapcar (lambda (x) (* 2 x)) '(1 2 3 4 5))   ; これじゃダメ?

これを評価してみると上の式でも下の式でも同じようになる. Webを見ていると,lambda自身がマクロで,(function (lambda ... ))に展開される.すでに#'されている場合と違う場合で展開を分けているのかな?

構造体(defstruct)

  • Cで言うところのstructというよりもclassに近い。
  • コンストラクタとアクセッサが自動で生成される。
    • コンストラクタ:make-<struct_name>
    • アクセッサ  :<struct_name>-<slot_name>
    • 述語     :<struct_name>-p
(defstruct my-struct
    (slot1 nil) ; (スロット名 初期値)
    (slot2 nil))
(setf x (make-my-struct)) ; コンストラクタ
(my-struct-slot1 x)       ; アクセッサ

ディレクトリ操作

  • カレントディレクトリの確認 (truname "./") #=> 処理系非依存 (sb-posix:getwd)
  • カレントディレクトリ移動(loadでのデフォルトパスネームは変わらない) (ccl::cd #p"/hoge/hoge/") # CCL (sb-posix:chdir #p"directory-path")
  • デフォルトのパスネームを変える (setf default-pathname-defaults #p"hogehoge")

乱数

  • common lispでの乱数の生成方法.
  • 一様乱数はrandで生成できる.
  • Alexandriaにはリストをシャッフルしたり正規分布からの抽出などの高機能なものもある。
  • TODO: 正規分布などの確率分布からのサンプリング方法
(alexandria:shuffle '(1 2 3 4 5)) ; 破壊的操作なので注意

リストからのランダム選択

(nth (random (length lst)) lst)

一様乱数

  • 仕様で規定されているようだけど,実装は規定されていない.
  • (random N) -> N以下の数をランダムに出力。Nが整数なら整数,少数なら少数を出力する。
  • 乱数はrandom-stateに従って生成される。random-stateは常に更新され続けるので,乱数の種を渡すときはrandom-stateを設定すればいい。(make-random-state state)で設定。
  • 各処理系の実装は下記
    • SBCL, CMUCL, ECL:MT19937 周期は219937
    • Clisp:Linear COngruential Generator 周期は264
    • CCL:MRG31k3p(Combined multiple recursive generator) 周期は2185
  • CCLでメルセンヌ・ツイスタを使いたい場合などは,mt19937パッケージを使う。CMUCLの実装をポータブルにしたものらしい。 (ql:quickload :mt19937) (mt19937 N)で同じことができる。

多値

  • 多値を返す時はvalues,それをmultiple-value-bindで受け取る.
  • 多値はリストじゃないことに注意.
  • リストで受け取りたいときはmultiple-value-listで受け取る.
  • multiple-value-bindでバインドされる変数のスコープはmultiple-value-bindの中だけなことに注意.

文字列の数値への変換

  • 整数への変換ならparse-integer.
  • 有理数浮動小数点への変換も含むなら,read-from-string.
(parse-integer "3")
(read-from-string "3.0")

ファイル

  • openは使わずにwith-open-fileでやる.
  • printとreadの対称性を使う.printはS式をreadが読み込み得る形式で書き出す.
;; 書込テンプレート
(defun save-file (filename)
  (with-open-file (fout filename
            :direction :output
            :if-exists :supersede)
    (with-standard-io-syntax
      (print *data* fout))))

行ごとの処理

  • 行毎に処理したい場合はwrite-lineとread-lineの対称性を使う.
  • read-lineは改行までを読み込んで,改行文字を削除した文字列を返す.
  • read-lineは多値を返す関数で,2つ目に終端が改行文字で終わってない場合はtを,改行文字の場合はnilを返す.
  • 第2引数がファイル末尾に達した時にエラーを返すか否か.デフォルトがtになっているので,nilを入れてエラー発生しないようにする.
;; file
(with-open-file (fout "./tmp.dat" :direction :output)
  (write-line "hogehoge" fout)
  (write-line "foo" fout)
  (write-line "bar" fout))

(with-open-file (fin "./tmp.dat" :direction :input)
  (loop for line = (read-line fin nil)
       while line
       do (format t "~s~%" line)))

ファイルのロード(load)

  • loadすると、そのファイルを読み込んでトップレベルを評価する。
; hogeディレクトリのfoo.lispをロードする
(load "hoge/foo.lisp")