Common Lisp

概要

Common LispはProgramming LanguageLISPの方言の1つ。 Lispファミリーではもっともメジャー。

Memo

TODO O’Reilly Japan - Land of Lisp   Read

_

  • 46, 48, 96, 103, 106, 132

数あてゲーム

(defparameter *small* 1)
*small*

1

(defparameter *foo* 5)
*foo*
(defparameter *foo* 6)
*foo*

6

(defun guess-my-number ()
  (ash (+ *small* *big*) -1))

GUESS-MY-NUMBER

(guess-my-number)

50

(defun smaller ()
  (setf *big* (1- (guess-my-number)))
  (guess-my-number))
(defun bigger ()
  (setf *small* (1+ (guess-my-number)))
  (guess-my-number))

BIGGER

(bigger)

75

(defun start-over ()
  (defparameter *small* 1)
  (defparameter *big* 100)
  (guess-my-number))
(start-over)

50

基本関数

(expt 53 53)

24356848165022712132477606520104725518533453128685640844505130879576720609150223301256150373

(/ 4 6)

2/3

(princ "aaaa")

aaaa

Lispには、コードモードとデータモードがある。 通常はコードモード、シングルクォートがつくとデータモード。

(car (cdr '(pork beef chicken)))
(cadr '(pork beef chicken))

名前はリストにしたときの順番になっている。 つまり評価するときの意味としては逆になる。 cadrはcdr+carである。

andやorは真偽値演算だけでなく、条件判断としても使える。

(and *file-modified* (ask-user-about-saving) (save-file))

member関数の返り値は部分リストになっている。もしマッチしたものを返すだったらnilを探すとき偽になってしまうから。

(if (member nil '(3 4 nil 5))
    'nil-is-in-the-list
    'nil-is-not-in-the-list)
  • シンボル同士は常に eq で比較する
  • シンボル同士の比較でなければ equal で比較する

描写する

  • ゲームに限らずほとんどの実用プログラムでは、出力される情報は単なるテキストよりもはるかに複雑な構造をとる。HTML, PDF, グラフィック
  • 元となるデータ構造を出力形式に縛られない形で最初から持っておけば、プログラミング言語の得意な点を活かしたコーディングができる。LISPの場合操作がしやすいのはシンボルとリストだから、可能な限り、プログラムを設計する際にこれらのデータ型で処理できないかを考える
(defparameter *nodes* '((living-room (you are in the living-room.
                                      a wizard is snoring loudly on the couch.))
                        (garden (your are in a beautiful garden.
                                 there is a well in front of you.))
                        (attic (you are in the attic.
                                there is a giant welding torch in the corner.))))

NODES

(assoc 'garden *nodes*)

(GARDEN (YOUR ARE IN A BEAUTIFUL GARDEN. THERE IS A WELL IN FRONT OF YOU.))

(defun describe-location (location nodes)
  (cadr (assoc location nodes)))
(describe-location 'living-room *nodes*)

(YOU ARE IN THE LIVING-ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH.)

通り道を描写する。

(defparameter *edges* '((living-room (garden west door)
                                     (attic upstairs ladder))
                        (garden (living-room east door))
                        (attic (living-room downstairs ladder))))

EDGES

テキストをシンボルのリストとして表現しておいたおかげで、準クォートを使って文を構築するコードを簡潔に書ける。

(defun describe-path (edge)
  `(there is a ,(caddr edge) going ,(cadr edge) from here.))
(describe-path '(garden west door))

(THERE IS A DOOR GOING WEST FROM HERE.)

1つの場所からはいくつもの通り道が出ている可能性がある。 与えられた場所から出ているすべての*edges*データから探して描写する関数が必要。

(defun describe-paths (location edges)
  (apply #'append (mapcar #'describe-path (cdr (assoc location edges)))))
(describe-paths 'living-room *edges*)

(THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE.)

mapcarはよく使われる。引数に他の関数とリストを受け取って、リストの要素それぞれを引数として受け取った関数を呼び出す。

(mapcar #'sqrt '(1 2 3 4))

(1 1.4142135 1.7320508 2)

(mapcar #'car '((foo bar) (baz qux)))

(FOO BAZ)

#’はfunctionオペレータの略記。この記号を含む式は、内部的に変換される。

(mapcar (function car) '((foo bar) (baz qux)))

(FOO BAZ)

Common Lispでは関数を値として扱うときにfunctionオペレータを使ってそのことを明示しなければならない。 関数と変数で名前が衝突した場合にエラーを起こす可能性があるから。

(let ((car "Honda Civic"))
  (mapcar #'car '((foo bar) (baz qux))))

(FOO BAZ)

Schemeでは、変数と関数と名前空間が共通なので関数を値として渡す場合にfunctionオペレータで明示する必要はない。

(apply #'append '((mary had) (a) (little lamb)))

(MARY HAD A LITTLE LAMB)

(apply #'append '((THERE IS A DOOR GOING WEST FROM HERE.)
                  (THERE IS A LADDER GOING UPSTAIRS FROM HERE.)))

(THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE.)

目に見えるオブジェクトをリストする

ゲーム世界に存在するオブジェクトのリストを作る。

(defparameter *objects* '(whiskey bucket frog chain))

OBJECTS

オブジェクトとその場所をalistで表現する。

(defparameter *object-locations* '((whiskey living-room)
                                   (bucket living-room)
                                   (chain garden)
                                   (frog garden)))
*object-locations*

((WHISKEY LIVING-ROOM) (BUCKET LIVING-ROOM) (CHAIN GARDEN) (FROG GARDEN))

与えられた場所から見るオブジェクトのリスト。

(defun objects-at (loc objs obj-locs)
  (labels ((at-loc-p (obj)
             (eq (cadr (assoc obj obj-locs)) loc)))
    (remove-if-not #'at-loc-p objs)))

OBJECTS-AT

objects-atを使ってみる。

(objects-at 'living-room *objects* *object-locations*)

(WHISKEY BUCKET)

ある場所で見えるオブジェクトの一覧。

(defun describe-objects (loc objs obj-loc)
  (labels ((describe-obj (obj)
             `(you see a ,obj on the floor.)))
    (apply #'append (mapcar #'describe-obj (objects-at loc objs obj-loc)))))

DESCRIBE-OBJECTS

使ってみる。

(describe-objects 'living-room *objects* *object-locations*)

(YOU SEE A WHISKEY ON THE FLOOR. YOU SEE A BUCKET ON THE FLOOR.)

現在地を保持する

現在値を保持する変数を作る。

(defparameter *location* 'living-room)
*location*

LIVING-ROOM

プレイヤーがタイプするlook関数を作る。見えるものすべてを描写する。

(defun look ()
(append (describe-location *location* *nodes*)
      (describe-paths *location* *edges*)
      (describe-objects *location* *objects* *object-locations*)))
(look)

(YOU ARE IN THE LIVING-ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH. THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE. YOU SEE A WHISKEY ON THE FLOOR. YOU SEE A BUCKET ON THE FLOOR.)

look関数はグローバル変数を読むから、関数的ではない。

動き回る

(defun walk (direction)
  (let ((next (find direction
                    (cdr (assoc *location* *edges*))
                    :key #'cadr)))
    (if next
        (progn (setf *location* (car next))
               (look))
        '(you cannot go that way.))))

WALK

(find 'y '((5 x) (3 y) (7 z)) :key #'cadr)

(3 Y)

(find '3 '((5 x) (3 y) (7 z)) :key #'car)

(3 Y)

:key #’carはキーワード引数。 コロンで始まる名前、続く値で構成されている。

(walk 'west)

(YOUR ARE IN A BEAUTIFUL GARDEN. THERE IS A WELL IN FRONT OF YOU. THERE IS A DOOR GOING EAST FROM HERE. YOU SEE A FROG ON THE FLOOR. YOU SEE A CHAIN ON THE FLOOR.)

オブジェクトを手に取る

pushとassocを使うことで、alistの値が変更されたかのように見せることができる。

(defun pickup (object)
  (cond ((member object
                 (objects-at *location* *objects* *object-locations*))
         (push (list object 'body) *object-locations*)
         `(you are now carrying the ,object))
        (t '(you cannot get that.))))

PICKUP

(walk 'east)

(YOU ARE IN THE LIVING-ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH. THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE. YOU SEE A WHISKEY ON THE FLOOR. YOU SEE A BUCKET ON THE FLOOR.)

(pickup 'whiskey)

(YOU CANNOT GET THAT.)

alist中の値を置き換えたければ、新しい要素をリストにpushするだけでいい。 assocはもっとも新しい値だけを返すから。

(defun inventory ()
    (cons 'items- (objects-at 'body *objects* *object-locations*)))
(inventory)

(ITEMS-)

(defparameter *foo* '(1 2 3))
(push 7 *foo*)

(7 1 2 3)

(setf *foo* (cons 7 '(1 2 3)))

(7 1 2 3)

動作を試す。 居間に戻ってウィスキーを取る。

(walk 'east)

(YOU CANNOT GO THAT WAY.)

(pickup 'whiskey)

(YOU ARE NOW CARRYING THE WHISKEY)

テキストの表示と読み込み

(print "foo")

foo

(progn (print "this")
       (print "is")
       (print "a")
       (print "test"))

“this” “is” “a” “test”

(progn (prin1 "this")
       (prin1 "is")
       (prin1 "a")
       (prin1 "test"))

this“”is“”a“”test

prin1の方がやってることは少ないので、より基本的な関数であると言える。組み合わせの自由度も高く、したがって大規模なコードの中でよく見られる。

入力させて挨拶を返す関数。

(defun say-hello ()
  (print "Please type your name:")
  (let ((name (read)))
    (print "Nice to meet you, ")
    (print name)))

printはコンピュータ向け、princは人間向け。 printは元のデータを表示する。printcは文字列にして表示する。

ダブルクォートをつけなくていい改良版。

(defun say-hello()
  (princ "Please type your name:")
  (let ((name (read-line)))
    (princ "Nice to meet you, ")
    (princ name)))

SAY-HELLO

データの対称性

プログラムコードとデータを同じデータ構造を使って扱うプログラミング言語は、同図象性を持つ、と呼ばれる。

  • ’(+ 1 2) → データモード
  • (+ 1 2) → コードモード

evalは強力で、自己書き換えのプログラムを書くには役立つ。が、普段はほとんど使わない。

専用のインターフェースを追加する

専用のREPLを作るのは簡単にできる。

(defun game-repl ()
  (loop (print (eval (read)))))
(game-repl)

REPLでの実行。

CL-USER> (look)
(YOU ARE IN THE LIVING-ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH. THERE IS
 A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE. YOU
 SEE A BUCKET ON THE FLOOR.)

quit呼び出しを検知して、replを抜けられるようにする。

(defun game-repl ()
  (let ((cmd (game-read)))
    (unless (eq (car cmd) 'quit)
      (game-print (game-eval cmd))
      (game-repl))))

GAME-REPL

カッコをつけなくてもコマンド入力できるようにする。 walk east とタイプしたなら、(walk east) になる。

(defun game-read ()
  (let ((cmd (read-from-string
              (concatenate 'string "(" (read-line) ")"))))
    (flet ((quote-it (x)
             (list 'quote x)))
      (cons (car cmd) (mapcar #'quote-it (cdr cmd))))))

GAME-READ

game-evalではあらかじめ決めたコマンドだけを呼べるようにする。

(defparameter *allowed-commands* '(look walk pickup inventory))

(defun game-eval (sexp)
  (if (member (car sexp) *allowed-commands*)
      (eval sexp)
      '(i do not know that command.)))

GAME-EVAL

テキストをいい感じに変換する関数が必要。

(defun tweak-text (lst caps lit)
  (when lst
    (let ((item (car lst))
          (rest (cdr lst)))
      (cond ((eql item #\space) (cons item (tweak-text rest caps lit)))
            ((member item '(#\! #\? #\.)) (cons item (tweak-text rest t lit))) ;; 文章の先頭は、!,?,.,のあとに現れる
            ((eql item #\") (tweak-text rest caps (not lit)))
            (lit (cons  item (tweak-text rest nil lit)))
            (caps (cons (char-upcase item) (tweak-text rest nil lit)))
            (t (cons (char-downcase item) (tweak-text rest nil nil))))))) ;; どの条件も満たさなければ、小文字になる

(defun game-print (lst)
  (princ (coerce (tweak-text (coerce (string-trim "() "
                                                  (prin1-to-string lst))
                                     'list)
                             t
                             nil)
                 'string)
         (fresh-line)))

GAME-PRINT

途中で大文字が出てくる場合に対応している。

(game-print '(not only does this sentence have a "comma," it also mentions the "iPad."))

Not only does this sentence have a comma, it also mentions the iPad.

Lambda

そもそもLispが産まれたのは、lambdaコマンドのためだった。

lambdaを使えば、名前を与えずに関数を作れる。

(mapcar (lambda (n) (/ n 2)) '(2 4 6))

(1 2 3)

(funcall (lambda (n) (/ n 2)) 2)

1

lambdaの引数は評価されずlambdaに渡される。つまり、lambdaは本物の関数ではない。これはマクロとよばれる。LISPの関数の引数は、関数自体が評価される前にすべて評価される。 lambdaが返す値は通常のLisp関数である。 多くの言語では、関数と値の世界を分けようとしている。Lispでは、この2つの世界をつなぐことができる。

関数を普通のデータのように受け渡しできるという機能は、とても便利である。純粋に数学的な意味では、lambdaが唯一のLispコマンドといえる。(ラムダ算法…lambdaを唯一のコマンドする理論的なプログラミング言語のようなもの。)

  • lambda形式はLispシステムの中でもっとも根源的なコマンドである
  • Lispの他の関数はlambdaの概念を元に導かれている
  • lambdaはLispのアイディアそのものが産まれた中心にある概念

奇妙なリスト

(cons 1 (cons 2 (cons 3 nil)))

(1 2 3)

(cons 1 (cons 2 3))

(1 2 . 3)

最後がnilではないことを明示するために.をつけている。 ドットリストは、対を表現するのによく使う。

リストの最後がリストの最初を指すような、循環しているリストもある。 遊ぶ前に準備する。

(setf *print-circle* t)

T

(defparameter foo (list 1 2 3))
(setf (cdddr foo) foo)

#1=(1 2 3 . #1#)

連想リスト

コンスセルから作られるデータ構造の中でも特に便利なのは、連想リスト。

(defparameter *drink-order* '((bill . double-espresso)
                              (lisa . small-drip-coffee)
                              (john . medium-latter)))
(assoc 'lisa *drink-order*)

(LISA . SMALL-DRIP-COFFEE)

追加。

(push '(lisa . large-mocha-with-whipped-cream) *drink-order*)

((LISA . LARGE-MOCHA-WITH-WHIPPED-CREAM) (BILL . DOUBLE-ESPRESSO) (LISA . SMALL-DRIP-COFFEE) (JOHN . MEDIUM-LATTER))

(assoc 'lisa *drink-order*)

(LISA . LARGE-MOCHA-WITH-WHIPPED-CREAM)

そのため、データの変更履歴をたどることも可能。

ノードの変換

グラフ構造を視覚的に表現するために、graphvizを使う。 フォーマットを出力するための関数を書く。

(defun dot-name (exp)
    (substitute-if #\_ (complement #'alphanumericp) (prin1-to-string exp)))
(dot-name 'foo!)

FOO_

substitute-ifは、与えられたテスト関数の結果によって値を置き換える関数。

(substitute-if #\e #'digit-char-p "I'm a l33t hack3r!")

I’m a leet hacker!

substitute-ifは、リストも処理できる。

(substitute-if 0 #'oddp '(1 2 3 4 5 6 7 8 9))

(0 2 0 4 0 6 0 8 0)

グラフのノードにラベルをつける。

(defparameter *max-label-length* 30)

(defun dot-label (exp)
  (if exp
      (let ((s (write-to-string exp :pretty nil)))
        (if (> (length s) *max-label-length*)
            (concatenate 'string (subseq s 0 (- *max-label-length* 3)) "...")
            s))
      ""))

DOT-LABEL

ノードのalistを取ってその情報をDOTの形で生成する関数を書く。

(defun nodes->dot (nodes)
  (mapc (lambda (node)
          (fresh-line)
          (princ (dot-name (car node)))
          (princ "[label=\"")
          (princ (dot-label node))
          (princ "\"];"))
        nodes))

NODES->DOT

(defparameter *wizard-edges* '((living-room (garden west door)
                         (attic upstairs ladder))
                        (garden (living-room east door))
                        (attic (living-room downstairs ladder))))

(defparameter *wizard-nodes* '((living-room (you are in the living-room.
                                      a wizard is snoring loudly on the couch.))
                        (garden (your are in a beautiful garden.
                                 there is a well in front of you.))
                        (attic (you are in the attic.
                                there is a giant welding torch in the corner.))))

WIZARD-NODES

(nodes->dot *wizard-nodes*)

LIVING_ROOM[label=“(LIVING-ROOM (YOU ARE IN TH…”]; GARDEN[label=“(GARDEN (YOUR ARE IN A BEAU…”]; ATTIC[label=“(ATTIC (YOU ARE IN THE ATTI…”];

次は、エッジをDOTの情報として書き出す。

(defun edges->dot (edges)
  (mapc (lambda (node)
          (mapc (lambda (edge)
                  (fresh-line)
                  (princ (dot-name (car node)))
                  (princ "->")
                  (princ (dot-name (car edge)))
                  (princ "[label=\"")
                  (princ (dot-label (cdr edge)))
                  (princ "\"];"))
                (cdr node)))
        edges))

EDGES->DOT

初めての人のためのLISP

_

Lispの考え方に焦点を当てた入門本。 解説で使われているのはCommon Lisp

cond

(cond ((>= 1 1) (print 0))
      ((= 0 0) (print 1)))
0

member

(defun my-member (x y)
  (cond ((null y) nil)
        ((eq x (car y)) t)
        (t (member x (cdr y)))))
(my-member 'a '(a b))

T

(my-member 'c '(a b))

NIL

assoc

(setq dict '((unum . 1) (duo . 2) (tria . 3)))
(assoc 'unum dict)

(UNUM . 1)

(defun my-assoc (x y)
  (cond ((null y) nil)
        ((eq x (caar y)) (car y))
        (t (assoc x (cdr y)))))
(my-assoc 'unum dict)

(UNUM . 1)

rassoc

(defun my-rassoc (x y)
  (cond ((null y) nil)
        ((eq x (cdar y)) (car y))
        (t (rassoc x (cdr y)))))
(my-rassoc 1 dict)

(UNUM . 1)

ドット記法で (reiko . (3 712 5648)) は、 (reiko 3 712 5678) と同じ。後ろの方がリストになっているとドットは書かない慣習。

Lispにおける式は、題付きリストといえる。 (関数 引数1 引数2 …) は、関数と引数のリストとのドット対、 (関数 . 引数のリスト) と考えることができる。

replaca

(rplaca '(1 1) 2)

(2 1)

(rplacd '(1 1) 2)

(1 . 2)

(defun update-phone (p x y)
    (rplacd (assoc x p) y)
    p  )

(setq dict '((unum . 1) (duo . 2) (tria . 3)))
(update-phone dict 'unum 111)

((UNUM . 111) (DUO . 2) (TRIA . 3))

remove

  (defun my-remove (x y)
    (cond ((null y) nil)
          ((eq (car y) x) (remove x (cdr y)))
          (t (cons (car y) (remove x (cdr y))))))
(my-remove 'mo '(to mo do mo mo to mo to mo))

(TO DO TO TO)

(defun my-delete-1 (x y)
  (setq y (cons 'dummy y))
  (my-del2 x (cdr y) y)
  (cdr y))

(defun my-del2 (x y z)
  (cond ((null y) nil)
        ((eq (car y) x) (rplacd z (cdr y)))
        (t (my-del2 x (cdr y) y))))
(my-delete-1 'mo '(mo mo mo to to to))

(MO MO TO TO TO)

(defun my-delete (x y)
  (setq y (cons 'dummy y))
  (my-dela x y)
  (cdr y))

(defun my-dela (x y)
  (cond ((null (cdr y)) nil)
        ((eq (cadr y) x)
         (rplacd y (cddr y))
         (my-dela x (cdr y)))
  (t (my-dela x (cdr y)))))

(my-delete 'mo '(mo to mo to))

(TO TO TO)

nreverse

(nreverse '(A B C))

(C B A)

(defun my-nreverse (x)
  (nrev2 x nil))

(defun nrev2 (x r)
  (cond ((null x) r)
        (t (rplacd x r)
           (nrev2 (cdr x) x))))
(my-nreverse '(A B C))

(A)

特殊形式prog1。 (prog1 式1 式2 式3 …) は返す値が式1の値。これを使って修正する。

(defun nrev2 (x r)
  (cond ((null x) r)
        (t (prog1 (nrev2 (cdr x) x)
             (rplacd x r)))))
(my-nreverse '(A B C))

(C B A)

破壊的関数

nreverseは破壊的。

(setq numl '(1 2 3))
(nreverse numl)

(3 2 1)

numl

(1)

破壊的関数にはsetqを使うとよい。

(setq numl '(1 2 3))
(setq numl (nreverse numl))
numl

(3 2 1)

append, nconc

appendの破壊版がnconc。

(setq numl '(1 2 3))
(append numl 1)
numl

(1 2 3)

(setq numl '(1 2 3))
(nconc numl 1)
numl

(1 2 3 . 1)

(defun my-nconc (x y)
  (cond ((null x) y)
        (t (rplacd (last x) y) x)))
(my-nconc '(1 2 3) 1)

(1 2 3 . 1)

last

(defun my-last (x)
  (cond ((null x) nil)
        (t (my-last2 x))))

(defun my-last2 (x)
  (cond ((null (cdr x)) x)
        (t (my-last2 (cdr x)))))

(my-last '(1 2 3))

(3)

subst

(subst 'a 'b '(a b (a b (b ba) nil a)))

(A A (A A (A BA) NIL A))

(defun my-subst (new old tree)
  (cond ((eq old Tree) new)
        ((atom tree) tree)
        (t (cons (subst new old (car tree))
                 (subst new old (cdr tree))))))
(my-subst 'a 'b '(a b a b))

(A A A A)

(subst 'kk nil '(a b (b ba) nil a))

(A B (B BA . KK) KK A . KK)

consを使っているので、新しいリストを作っていることになる。

(subst 'a 'b '(a a a))

(A A A)

何もやらないときはcopy関数の定義と同じ。

(defun my-copy (tree)
  (cond ((atom tree) tree)
        (t (cons (my-copy (car tree))
                 (my-copy (cdr tree))))))
(my-copy '(a a a))

(A A A)

今風スタイルなsubst。

(defun my-subst (new old tree)
  (cond ((eq old tree) new)
        ((atom tree) tree)
        (t (let ((a (my-subst new old (car tree)))
                 (d (my-subst new old (cdr tree))))
             (cond ((and (eq a (car tree))
                         (eq d (cdr tree)))
                    tree)
                   (t (cons a d)))))))
(my-subst 'a 'b '(a b))

(A A)

複数種類の置き換えをしたい。

(defun my-sublis (alist tree)
  (let ((pair (assoc tree alist)))
    (cond (pair (cdr pair))
          ((atom tree) tree)
          (t (let ((a (my-sublis alist (car tree)))
                   (d (my-sublis alist (cdr tree))))
               (cond ((and (eq a (car tree))
                           (eq d (cdr Tree)))
                      tree)
                     (t (cons a d))))))))
(my-sublis '((unum . 1) (duo . 2) (tria . 3)) '(unum duo tria unum (unum tria)))

(1 2 3 1 (1 3))

defsubst

defsubstが使われるとき。

まずifを定義してみる(これはうまくいかない)。

(defun my-if (p x y)
  (cond (p x)
        (t y)))

(setq x 4)
(setq flag t)
(my-if flag (setq x (+ x 1)) (setq x (+ x 2))) ;; => 5
x ;; => 7

7

(defsubst my-if (p x y)
  (cond (p x)
        (t y)))

;; (setq x 4)
;; (setq flag t)
;; (my-if flag (setq x (+ x 1)) (setq x (+ x 2)))

余剰変数: 変数が不定個の引数をリストに束ねて受け取ること。

(defun my-list (&rest x) x)
(my-list 1 1)

(1 1)

defmacro

(defmacro my-first (x)
  (list 'car x))
(my-first (list 1 2 3))

1

(my-first (list 1 2 3)) は、 (car (list 1 2 3)) に置き換わるように見える。

試しにdefunでやってみると、できない。

(defun my-first (x)
  (list 'car x))
(my-first '(1 2 3)) ;; '(car (1 2 3)) と同じ

(CAR (1 2 3))

condをマクロ定義してみる。

(defmacro my-cond (&rest clauses)
  (expand-cond clauses))

(defun expand-cond (clauses)
  (my-cond ((null clauses) nil)
        ((eq (caar clauses) 't)
         (cons 'progn (cdar clauses)))
        (t (list 'if
                 (caar clauses)
                 (cons 'progn (cdar clauses))
                 (expand-cond (cdr clauses))))))
(my-cond (1 '(1))
         (t '(t)))

(1)

backquoteをつけると、quoteと違ってS式がコピーされる。 コピーの途中で、コンマのついた部分S式があるとそれを評価する。 これを用いてfirstの定義を書き直す。

(defmacro my-first (x)
  `(car ,x))
(my-first '(1 2 3))

1

よく見るパターンをマクロ化する。

(cond ((null なんとか) どうする1)
      (t どうする2))
(defmacro if-null (nan dos1 dos2)
  `(cond ((null ,nan) ,dos1)
         (t ,dos2)))
(defun my-even (x)
  (if-null (= (mod x 2) 1) t nil))
(my-even 2)

T

pop

よく使うマクロ2つ。

(defmacro my-pop (x)
  `(prog1 (car ,x) (setq ,x (cdr ,x))))
(defmacro my-push (y x)
  `(setq ,x (cons ,y ,x)))
(setq pop-test '(1 2 3))
(my-pop pop-test)

1

pop-test

(2 3)

(defmacro image (var list &rest forms)
  `(let (($list$ ,list)
         ($r$ nil)
         (,var nil))
    (while ($list$ (nreverse $r$))
     (setq ,var (pop $list$))
     (push (progn ,@forms) $r$))))
(image i (list 1 2 3 4) (* i i)) ;; => (1 4 9 16)になるはずだが動かない
;; i をrubyでいうブロック引数とするように定義するマクロ
;; このようにもともとの特殊形式と同じように自由に定義できるのがLispらしさ

文字列

(eq "tide" "tide")

NIL

(equal "tide" "tide")

T

alist

(defun my-get (symbol property)
  (let ((plist (symbol-plist symbol)))
    (loop (until (null plist) nil)
       (until (eq (car plist) property) (cadr plist))
       (setq plist (cddr plist)))))

MY-GET

(putprop 'foo
         (cons (symbol-function 'foo)
               (get 'foo 'old-definition))
         'old-definition)

lambdaはdefunのように関数を定義する特殊形式ではない。 lambdaはcarにあるリストが関数実体を示す単なる標識。 defunとは、関数実体に名前をつける関数といえる。

(apply (lambda (x y) (+ (* x x) (* y y))) (list 3 4))

25

簡単な例。

(apply '+ '(1 2 3 4))

10

funcallで書く。

(funcall '+ 1 2 3 4)

10

mapcar

(defun my-mapcar (fn mlist)
  (cond ((null mlist) nil)
        (t (cons (funcall fn (car mlist))
                 (mapcar fn (cdr mlist))))))
(my-mapcar #'sqrt '(1 2 3))

(1 1.4142135 1.7320508)

(defun my-maplist (fn mlist)
  (cond ((null mlist) nil)
        (t (cons (funcall fn mlist) ;; mapcar との違いはここだけ。carではなくリストに対してfnをapplyする
                 (maplist fn (cdr mlist))))))
(my-maplist #'append '(1 2 3))

((1 2 3) (2 3) (3))

(reverse
 (my-maplist (lambda (x) (apply #'+ x))
             (reverse '(10 5 6 12 3 5 9 7 0 4 2 15))))

(10 15 21 33 36 41 50 57 57 61 63 78)

sort

(setq monthly-sales
      '((Jan . 10) (Feb . 5) (Mar . 6) (Apr . 12) (May . 3)(Jun . 5) (Jul . 9) (Aug . 7) (Sep . 0) (Oct . 4)(Nov . 2) (Dec . 15)))
(sort monthly-sales #'(lambda (x y) (> (cdr x) (cdr y))))

((DEC . 15) (APR . 12) (JAN . 10) (JUL . 9) (AUG . 7) (MAR . 6) (FEB . 5) (JUN . 5) (OCT . 4) (MAY . 3) (NOV . 2) (SEP . 0))

eval

evalを実装することで理解する。 スコープあたりの核となる部分を実装しているのだが、よくわからない。

(defun eval (form)
  (cond
    ((or (null form) (numberp form) (stringp form)) form) ; nil, 数, 文字列の値はそれ自身
    ((symbolp form) (variable-value form)) ; シンボルは変数と解釈される
    ((member (car form)
             '(quote cond setq prog progn prog1 prog2 go let let* if do do* defun defmacro function ...))
     (eval-special-form form)) ; 特殊形式の処理(ここから先はセルとわかっている)
    ((and (consp (car Form)) ; ラムダ式
          (eq (caar form) 'lambda))
     (apply (car form) (evlis (cdr form))))
    ((function-symbol-p (car form)  ; 関数シンボル
                        (evlis (cdr form))))
    ((macro-symbol-p (car form)) ; マクロ呼び出し。ただしbackquoteには未対応
     (eval (apply (macro-function (car form)) (cdr form))))
    (t (error 'concat-evaluate form))))
;; 引数を順次評価してリストにする(実際はリストを作らずスタックに積む ??)
(defun evlis (args)
  (cond ((null args) nil)
        (t (cons (eval (car args)) (evlis (cdr args))))))
(defun eval-special-form (form)
  (cond ((eq (car form) 'quote) (cadr form))
        ((eq (car form 'cond) (evcon (cdr form))))
        ((eq (car form) 'setq') ...)
        ((eq (car form) 'progn) (evprogn (cdr form)))))
(defun evcon (clauses)
  (cond
    ;; もうcond節が残ってなければnilを返す
    ((null clauses) nil)
    ;; 述語が真であれば、帰結の暗黙のprognを順次評価する
    ((eval (caar clauses))
     (evprogn (cdr clauses)))
    ;; 次のcond節を調べる
    (t (evcon (cdr clauses)))))
(defun evprogn (forms)
  (cond
    ((null forms) nil)
    ((null (cdr forms)) (eval (car forms)))
    (t (eval (car forms)) (evprogn (cdr forms)))))

キーワード

Common Lispでは名前空間のことをシンボル・パッケージあるいは単にパッケージと呼ぶ。シンボルはどれかのパッケージに属する。 Common Lispで標準的に用意されているのはlisp, user, keyword, systemの4つ。

carと使うときは、実際にはlisp:carとしている。文脈によって自動で決定されるので毎回lisp:を打つ必要はない。

キーワード引数は名前空間のうち、keywordを使う。特別扱いされ、keyword: が :だけに省略できる。 なので keyword:direction と:direction は同じ意味である。キーワードを評価すると、つねにそれ自身を値として返すので、quoteする必要がない。

Tasks

TODO 魔法言語 リリカル☆Lisp

nscripterと萌えキャラでLispが学べる…。

Reference

Archives

DONE Road to Common Lisp

Lispの学び方、おすすめ本の紹介。