「Land of lisp」を読んだのでCommon Lispのまとめ
Land of lispを読んだので、Common Lispについてまとめてみます(以下Common LispのことをLispと表記しています)。 一応私はLisp初心者なので間違いがあるかもしれません。
clisp
まず、Lispの処理系ですが以下のコマンドで一発でインストールできます (clispがメジャーらしい)。
>brew install clisp
>choco install clisp
clispはREPLもできますが、以下のようにソースファイルをインタプリタすることができます。
>clisp a.lisp
clispはソースファイルをコンパイルできますが、出力されるのはネイティブコードではなくバイトコードになります。
>clisp -c a.lisp -o a.fas >clisp a.fas
Lispについて
Lispのコードは簡単にいうとポーランド記法の式を括弧で括って表現されたものです。括弧で括られたものをリストと呼びます。
(+ 1 2) ; 3
コメントアウト
; これはコメントです。
コマンドモードとデータモード
Lispにはコマンドモードとデータモードというものがあって、以下のようなリストはコマンドモードという状態下にあります。
コマンドモードでのリストをフォームとも呼び、フォームの1つ目の要素(ここでは+
に当たる)をコマンドと呼びます。
このコマンドは関数やマクロなど特別なものでないといけません。
(+ 1 2) ; 3
データモードはリストに'
を付けた形となります。コマンドモードと異なり、1つ目の要素がコマンドである必要はありません。
なのでデータモードでは、リストとしてそのまま評価されます。
'(+ 1 2) ; (+ 1 2) '(1 2 3) ; (1 2 3) '(1 2 3 (4 5)) ; (1 2 3 (4 5)) ; '(...)内のものはすべてデータモードで評価される。
シンボル
Lispにはシンボルという独立した値を表すトークンが存在します。
シンボルに使える文字はアルファベット、数字、そして+
-
/
*
=
<
>
?
!
_
になります。
また、シンボルは基本的にアルファベットの大文字小文字を区別しません。
ただし、|
を使うことで大文字小文字を区別したシンボルを作成することも出来ます。
(princ 'hoge) ; HOGE (princ '|Hello World|) ; Hello World
数値
Lispの数値には整数と浮動小数点数の2種類が存在します。
小数点が含まれていれば浮動小数点数、小数点が含まれていなければ整数として扱われます。
また、Lispでは1
と1.0
が区別されます。
(princ 12) ; 12 (princ 1.23) ; 1.23
文字列
Lispの文字列は文字の集まりを"
で括って表します。
\
と"
を含めたい場合には\
でエスケープします。
(princ "hello") ; hello (print "hello") ; "hello"
文字
Lispの文字リテラルは#\
を付けることで表すことが出来ます。
特殊な文字(改行、タブ、スペース、など)はそれぞれ#\newline
、#\tab
、#\space
で表すことが出来ます。
(princ #\c) ; c (princ #\newline) (princ #\tab) (princ #\space)
グローバル変数
グローバル変数を定義するにはdefparameter
もしくはdefvar
を使います。
(defparameter *hoge* 12)
(defvar *hoge* (+ 3 4))
defparameter
とdefvar
の違いは複数回呼ばれた時に上書きされるかどうかです。
defparameter
はどんどん上書きしていくのに対して、
(defparameter *hoge* 1) (print *hoge*) ; 1 (defparameter *hoge* 2) (print *hoge*) ; 2
defvar
は初回の値を保ち続けます。
(defvar *hoge* 1) (print *hoge*) ; 1 (defvar *hoge* 2) (print *hoge*) ; 1
Lispではグローバル変数を*
で括る文化があるらしい。
また、Lispではグローバル変数のことをダイナミック変数やスペシャル変数とも呼びます。
関数の定義
関数を定義するには(defun 関数名(引数) 本体)
の形式で定義することができます。
(defun add(a b) (+ a b)) (add 1 2) ; 3
ローカル変数とローカル関数
ローカル変数を定義するには(let ((変数名 値) ...) 本体)
の形式で定義することができます。
(let ((x 3) (y 4)) (* x y)) ; 12
ローカル関数を定義するには(flet ((関数名(引数) 本体) ...) 本体)
の形式で定義することができます。
(flet ( (f(x) (1+ x)) ) (f 3)) ; 4
flet
は同レベルで定義されたローカル関数を参照することができません(再帰すらできません)。
なので、同レベルで定義されたローカル関数を参照するローカル関数を定義するにはlabels
を使います。
別のローカル関数を呼ぶ
(labels ( (f(x) (1- x)) (g(x) (* (f x) 2)) ) (g 3)) ; 4
自身のローカル関数を呼ぶ
(labels ( (f(xs) (if xs (+ (car xs) (f (cdr xs))) 0))) (f '(1 2 3))) ; 6
コンスセル
Lispにはコンスセルという単位があります。 コンスセルは値を格納できる箱(スロットと呼ばれる)が2つあるようなものです。
コンスセルのイメージ
[|] / \ 1 2
ちなみにリストはコンスセルの集まりです。
リストはコンスセルが連続して最後の要素がnil
という特別な値で終わるもののことを指します。
リストのイメージ
[|] / \ 1 [|] / \ 2 [|] / \ 3 nil
コンスセルを作成するにはcons
か.
で作成することが出来ます。
(cons 1 2) ; (1 . 2) '(1 . 2) ; (1 . 2)
リストはコンスセルの集まりなので、以下のようにリスト作ることも出来ます。
'(1 . (2 . nil)) ; (1 2) '(1 2) ; (1 2)
真理値
Lispには特別なシンボルT
とnil
があります。T
が真値、nil
が偽値と評価されます。
(if T 1 2) ; 1 (if nil 1 2) ; 2
実はLispではnil
、'nil
、()
、'()
が同じ意味を持ち偽値と評価され、これ以外のすべて値が真値と評価されます。
(if '(1 2) 1 2) ; 1 (if () 1 2) ; 2
carとcdr
Lispにはリストから値を取り出す関数car
とcdr
があります。
car
は先頭の要素を返し、cdr
は残りの要素をリストで返します。
(car '(1 2 3 4 5)) ; 1 (cdr '(1 2 3 4 5)) ; (2 3 4 5)
car
もcdr
も、正確にはコンスセルを扱う関数なので以下のようにコンスセルから値を取り出すことも出来ます。
(car '(1 . 2)) ; 1 (cdr '(1 . 2)) ; 2
リストにcdr
を適用すると末尾にnil
が付属するため結果がリストになりますが、
コンスセルにcdr
を適用しても末尾にnil
が付属しないため結果がリストになるとは限りません。
(cdr '(1 2)) ; (2) (cdr '(1 . 2)) ; 2 (cdr '(1 . (2 . nil))) ; (2)
Lispにはcar
やcdr
のほかにcdar
のような/c[ad]+r/の関数が用意されています(どのくらいの長さまで用意されているかは処理系による)。
(cadr '(1 2 3 4)) ; 2 (cadddr '(1 2 3 4)) ; 4 (cddddr '(1 2 3 4)) ; nil (caar '((1 2) 3 4)) ; 1
eqとequal
Lispには同じかどうか比較する関数がいくつか存在します。 とりあえず、以下のルールにしたがって使えばよいらしい。
- シンボル同士は常に
eq
で比較すべし。 - シンボル同士でなければ
equal
を使え。
ちなみにequal
は内部で型分岐をしていて、数値なら=
、文字列ならstring-equal
、文字ならchar-equal
を使っています。
なので、処理速度を求めるなら、これらを直接呼んだ方が良いです。
準クオート
Lispの'
によってコマンドモードからデータモードに移行することはできますが、
データモードからコマンドモードに移行することはできません。
そのため、Lispには`
と,
を使ってデータモードからコマンドモードへと移行出来る仕組みが存在します。
`(1 2 ,(car '(3 4)) 5) ; (1 2 3 5)
このように、`
でデータモードが開始され、,
で一時的にコマンドモードへと戻ることが出来ます。
関数オペレータ
Lispは関数を値として扱うことが出来ます。例えば、mapcar
関数は関数とリストを1つずつ受け取り、
そのリストの各要素に関数を適用した新たなリストを返す関数です。
その際、関数を値として認識させるために関数オペレータ#'
もしくはfunction
関数を使用します。
mapcar
関数のような関数を値として扱うような関数を高階関数と呼びます。
(mapcar #'sqrt '(1 2 3 4)) ; (1 1.4142135 1.7320508 2) (mapcar (function car) '((a b) (c d) (e f) (g h))) ; (A C E G)
ラムダ式
Lispで関数を定義するにはdefun
関数のような関数を使います。しかし、これらの関数は関数名を指定しなくてはなりません。
高階関数を使う際にわざわざ関数名を指定するのは億劫なので、Lispにはラムダ式というlambda
を使って即座に名前なしの関数を作成することが出来ます。
(mapcar (lambda (x) (* x 2)) '(1 2 3 4)) ; (2 4 6 8)
循環リスト
リストの末尾のnil
をリストの先頭にしてやることで循環リストという循環したコンスセルを作成することが出来ます。
循環リストのイメージ
+--------+ v | [|] | / \ | 1 [|] | / \ | 2 [|] | / \ | 3 +--+
; clispの処理系に「これから循環リストを表示するよ」と伝える必要がある。 (setf *print-circle* t) (defparameter *foo* '(1 2 3 4)) (setf (cddddr *foo*) *foo*) (print *foo*) ; #1=(1 2 3 4 . #1#)
配列
Lispにはリストの他に配列も用意されています。配列はリストより要素に速くアクセスすることが出来ます。
配列を作成するにはmake-array
で作成することが出来ます。引数にはその配列のサイズを指定します。
配列の要素を参照する方法はaref
を使い、値を設定するにはaref
とsetf
を使います。
(defparameter *ary* (make-array 3)) (print *ary*) ; #(nil nil nil) (setf (aref *ary* 1) 3) (print *ary*) ; #(nil 3 nil) (print (aref *ary* 1)) ; 3
ハッシュテーブル
Lispにはハッシュテーブルも用意されています(連想リストとは別物です)。
ハッシュテーブルを作成するにはmake-hash-table
を使います。
ハッシュテーブルのキーを参照する方法はgethash
を使い、キーの値を設定するにはgethash
とsetf
を使います。
(defparameter *hash* (make-hash-table)) (print *hash*) ; #S(HASH-TABLE :TEST FASTHASH-EQL) (setf (gethash 'foo *hash*) 3) (print (gethash 'foo *hash*)) ; 3
構造体
Lispには構造体も用意されています。
構造体を定義するには(defstruct 構造体名 メンバ...)
の形式で定義することができます。
構造体が定義されると自動的に構造体を作成する関数と各メンバを参照する関数が定義されます。
(defstruct person name age) (defparameter *p* (make-person :name "bob" :age 32)) (print *p*) ; #S(PERSON :NAME "bob" :AGE 32) (print (person-age *p*)) ; 32
マクロ
LispにはC言語の#define
のような強力なマクロが存在します。
マクロを定義するには(defmacro マクロ名 (引数) 本体)
とdefun
と似た感じで定義出来ます。
マクロ例1
(defmacro m1 (f n1 &rest rest) `(+ ,@rest) ) (print (m1 cdr 1 2 3)) ; 5
&rest
はrest
に残りの引数を1つのリストにしますよと伝えるためのものです。
なのでここでは、f
にcdr
、n1
に1
、rest
に(2 3)
が入ります。
ここでrest
に@
を付け加えることで2 3
と括弧を取り除いた状態で展開することが出来ます。
よって、このマクロを展開すると(+ 2 3)
という形になり、5
が出力されます。
マクロ例2
(defmacro m2 (&rest rest) `(list ,@rest) ) (print (macroexpand (m2 1 2 3))) ; (1 2 3)
macroexpand
はマクロ展開後の状態を文字列として返す便利なマクロです。
マクロ例3
(defmacro m3 (f n1 &rest rest) `(,f ',rest) ) (print (m3 cdr 1 2 3 4)) ; (3 4)
主な関数やマクロ
time
引数に指定された要素の評価した処理時間を求める。
(time (+ 1 2)) ; Real time: 2.1E-5 sec. ; Run time: 1.7E-5 sec. ; Space: 0 Bytes
substitute-if
引数に指定された述語にマッチした要素を置換して返す。
(substitute-if #\- (lambda (x) (equal x #\3)) "1234") ; "12-4" (substitute-if 'x (lambda (x) (eq x 'c)) '(a b c)) ; (A B X)
assoc
連想リスト(alistと呼ばれる)からキーにマッチする要素を返す。
(assoc 'hoge '((foo (1 2)) (hoge (3 4)))) ; (HOGE (3 4))
push
リストを保持している変数の先頭に要素を追加する。
(defparameter *foo* '((a 1) (b 2))) (princ *foo*) ; ((a 1) (b 2)) (push '(c 3) *foo*) (princ *foo*) ; ((c 3) (a 1) (b 2))
setf
変数に値を代入する。
(defparameter *foo* '(1 4)) (setf *foo* (cons 7 *foo*)) (princ *foo*) ; (7 1 4)
find
リストの各要素に:key
で指定された関数を適用した結果に対して第1引数の要素を探す。
(find 'b '((a 1) (b 2) (c 3) (d 4)) :key #'car) ; (B 2) (find 'e '((a 1) (b 2) (c 3) (d 4)) :key #'car) ; nil
remove-if-not
リストの各要素に関数を適用した結果が偽の要素を取り除いた新たなリストを返す。
; 奇数でない要素を取り除く。 (remove-if-not #'oddp '(1 2 3 4)) ; (1 3)
mapcar
リストの各要素に関数を適用した新たなリストを返す。
(mapcar #'sqrt '(1 2 3 4)) ; (1 1.4142135 1.7320508 2)
sqrt
平方根を計算する。
(sqrt 2) ; 1.4142135
expt
べき乗を計算する。
(expt 2 3) ; 8
if
条件分岐を行う。
(if T 1 2) ; 1 (if nil 1 2) ; 2 (if () (princ "good") (princ "bad")) ; bad
when
真の場合のみ処理を実施する。
(when T (princ "bad")) ; bad (when nil (princ "good"))
unless
偽の場合のみ処理を実施する。
(unless T (princ "bad")) (unless nil (princ "good")) ; good
cond
条件分岐を行う。
(let ((x 2)) (cond ((eq x 1) "A") ((eq x 2) "B") ((eq x 3) "C") (t "-"))) ; "B"
case
条件分岐を行う。
(let ((x 2)) (case x (1 "A") (2 "B") (3 "C") (otherwise "-"))) ; "B"
and
AND演算を行う。
(and) ; T (and T nil) ; nil (and T T T T) ; T (and nil nil nil) ; nil
or
OR演算を行う。
(or) ; nil (or T nil) ; T (or T T T T) ; T (or nil nil nil) ; nil
append
リスト同士を結合する。
(append '(1 2) '(3 4)) ; (1 2 3 4) (append '(1 2) 3) ; (1 2 . 3)
member
リストから要素を探す。 もし見つかったなら、その位置から末尾までのリストを返す。
(member 1 '(1 2 3 4)) ; (1 2 3 4) (member 3 '(1 2 3 4)) ; (3 4) (member 0 '(1 2 3 4)) ; nil (member 5 '(1 2 3 4)) ; nil
find-if
リストから述語にマッチする最初に見つかった要素を返す。
(find-if 'oddp '(2 3 2 2)) ; 3
progn
複数の処理を1つにまとめる。
(if () (princ "good") (progn (princ "too") (princ " ") (princ "bad")) ) ; too bad
oddp
奇数ならT
を返し、偶数ならnil
を返す。
(oddp 1) ; T (oddp 2) ; nil
evenp
偶数ならT
を返し、奇数ならnil
を返す。
(evenp 2) ; T (evenp 1) ; nil
random
0からN-1の間の値をランダムに返す。
(random 4) ; 3 (random 4) ; 0 (random 4) ; 2 (random 4) ; 1
length
リストの長さを返す。
(length '(1 2 3 4)) ; 4
nth
N番目の要素を返す(0オリジン)。
(nth 3 '(1 2 3 4)) ; 4 (nth 0 '(1 2 3 4)) ; 1 (nth 2 '(1 2 3 4)) ; 3 (nth 6 '(1 2 3 4)) ; nil
list
引数に与えられたものからリストを作ります。
(list 1 2 3 4) ; (1 2 3 4)
load
別のソースファイルを読み込みます。
(load "b.lisp")
read
ユーザーからEnterキーが押されるまで入力を受けつける。 入力された文字列に応じて、シンボル、数値、文字列などに変換して返す。
(read) hoge ; HOGE (read) 123 ; 123 (read) "key" ; "key"
read-from-string
read
関数の文字列版。
文字列に応じて、シンボル、数値、文字列などに変換して返す。
(read-from-string "hoge") ; HOGE (read-from-string "123") ; 123 (read-from-string "\"key\"") ; "key"
concatenate
引数に応じてシーケンス同士の連結を行う。
(concatenate 'string "hoge" "foo") ; "hogefoo" (concatenate 'list '(2) '(1)) ; (2 1)
char-downcase
文字を小文字に変換する。
(char-downcase #\C) ; #\c
char-downcase
文字を大文字に変換する。
(char-upcase #\c) ; #\C
eval
引数に指定されたデータを評価し、その値を返す。
(eval '(+ 1 2)) ; 3
read-line
ユーザーからEnterキーが押されるまで入力を受ける。 入力された文字列を1つの文字列として解釈する。
(read-line) hoge ; "hoge" (read-line) 123 ; "123" (read-line) "key" ; "\"key\""
print
半角スペースと改行付きで値をそのまま標準出力する。
(print "hey") ; "hey"
prin1
print
関数の半角スペースと改行が付かない版。
(prin1 "hey") ; "hey"
princ
値を人間に分かりやすい形で標準出力する。
(princ "hey") ; hey
fresh-line
直前の文字が改行出ない場合のみ、改行を出力する。
(fresh-line) ; (fresh-line) (fresh-line) (fresh-line) (fresh-line)
terpri
改行を出力する。
(terpri) ; (terpri) ; (terpri) ; (terpri) ; (terpri) ;
incf
1+
インクリメント。
(1+ 4) ; 5
decf
1-
デクリメント。
(1- 4) ; 3
ash
Bitシフト。
; 左に1bitシフトする。 (ash 11 1) ; 22 ; 右に1bitシフトする。 (ash 11 -1) ; 5
終わり
まだ、loopマクロ、format関数、ストリーム、など説明してない項目がありますが、ある程度はまとめられたと思うので終わり。