読者です 読者をやめる 読者になる 読者になる

「Land of lisp」を読んだのでCommon Lispのまとめ

Land of lispを読んだので、Common Lispについてまとめてみます(以下Common LispのことをLispと表記しています)。 一応私はLisp初心者なので間違いがあるかもしれません。

www.amazon.co.jp

clisp

まず、Lispの処理系ですが以下のコマンドで一発でインストールできます (clispがメジャーらしい)。

Mac

>brew install clisp

Windows

>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;コメントアウトすることができます。

; これはコメントです。

コマンドモードとデータモード

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では11.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))

defparameterdefvarの違いは複数回呼ばれた時に上書きされるかどうかです。

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には特別なシンボルTnilがあります。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にはリストから値を取り出す関数carcdrがあります。 carは先頭の要素を返し、cdrは残りの要素をリストで返します。

(car '(1 2 3 4 5))
; 1
(cdr '(1 2 3 4 5))
; (2 3 4 5) 

carcdrも、正確にはコンスセルを扱う関数なので以下のようにコンスセルから値を取り出すことも出来ます。

(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にはcarcdrのほかに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には同じかどうか比較する関数がいくつか存在します。 とりあえず、以下のルールにしたがって使えばよいらしい。

  1. シンボル同士は常にeqで比較すべし。
  2. シンボル同士でなければ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を使い、値を設定するにはarefsetfを使います。

(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を使い、キーの値を設定するにはgethashsetfを使います。

(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

&restrestに残りの引数を1つのリストにしますよと伝えるためのものです。 なのでここでは、fcdrn11rest(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関数ストリーム、など説明してない項目がありますが、ある程度はまとめられたと思うので終わり。