プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(9)

今日も疲れています。変なコードを書いてたらそれは疲れのせいです。
言い訳を済ませたので、今日もやっていきます。
「Land of Lisp」6章、この前作ったゲームに専用のインターフェースを作ります。

REPL

さ、LispではREPL(Read,Eval,Print,Loop)を作るのはとても簡単だそうです。
やってみます。

CL-USER> (defun my-repl()
	   (loop (print (eval (read)))))
MY-REPL
CL-USER> (my-repl)
(princ 'できてるかな?)
できてるかな?
できてるかな? (princ 'しょぼいけどできてる)
しょぼいけどできてる
しょぼいけどできてる

改行もしてくれませんができているようです。
普段使ってるREPLは凄いんですね。
C-c C-cでループから脱出します。

もっと良いやつを作ろう、ということで紹介されたのがこちらです。

CL-USER> (defun game-repl ()
	   (let ((cmd (game-read)))
	     (unless (eq (car cmd) 'quit)
	       (game-print (game-eval cmd))
	       (game-repl))))
; in: DEFUN GAME-REPL
;     (GAME-EVAL CMD)
; 
; caught STYLE-WARNING:
;   undefined function: GAME-EVAL

;     (GAME-PRINT (GAME-EVAL CMD))
; 
; caught STYLE-WARNING:
;   undefined function: GAME-PRINT

;     (GAME-READ)
; 
; caught STYLE-WARNING:
;   undefined function: GAME-READ
; 
; compilation unit finished
;   Undefined functions:
;     GAME-EVAL GAME-PRINT GAME-READ
;   caught 3 STYLE-WARNING conditions
GAME-REPL
CL-USER>

game-で始まってるコマンドはなんだよ、と思いながら写経していましたが、
まだ定義していないと怒られました。次のページから定義していくようです。
game-replは難しくはないですね。
cmdを定義し、game-readで読み込んだものを格納、
それがquitじゃなければgame-evalでcmdを評価、結果をgame-printで表示、
そしてgame-replを再度呼び出す。
quitが入力されたらそこらへんはやらないし再帰もしないのでそこで終わりです。

さあ、本番はここからです。まず専用のread関数を書いていきます!
普通のread関数をゲームに使うのに宜しくない点は2つあります。
・いちいちコマンドを括弧で囲まなきゃいけない。
・引数にはクオートをつけてシンボルとして渡さなきゃいけない。
自分だったらそんなテキストゲームあんまりやりたくないですね。
つまりgame-read関数には、walk 東 という入力を(walk '東)に補完して貰えば良いわけです。
確か作ったゲームのコマンドは全て引数は1個か無いかだったはずです。
そんな難しくないんじゃないかなあ。やってみますか。

CL-USER> (defun game-read ()
	   (let ((cmd (read-line)))
	     (princ cmd)))
GAME-READ
CL-USER> (game-read)
walk 東
walk 東
"walk 東"
CL-USER>

嫌な予感がしたのでいわゆるprintデバッグです。
普通のreadだとスペースの前までしか受け取ってくれないし、
read-lineだと"walk 東"という文字列になってしまいますね…。
リストの状態でないとcdrを使ったりできないし、そもそもある要素に
クオート足す方法がわかりませんでした。
参考書の答えはこちらです。

CL-USER> (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))))))
WARNING: redefining COMMON-LISP-USER::GAME-READ in DEFUN
GAME-READ
CL-USER> (game-read)
walk 東
(WALK ')
CL-USER>

むむむ…一個づつ理解します。
read-from-string…ちょっと使ってみます。

CL-USER> (read-from-string "今日はいい天気だなあ")
今日はいい天気だなあ
10
CL-USER>

文字列を受け取って…何を返してるんだ?
ダブルクォーテーションをなくした元文字列と文字数ですかね。
バイト数とかじゃなくて単純に文字数のようです。
2つ返ってきてるという理解で良いのかな。letでcmdに格納しているのは
最初の文字列なのは間違いなさそうですが、letは最初の返り値で設定してくれるのか。
CLHS: Function READ-FROM-STRING
https://lisphub.jp/common-lisp/cookbook/index.cgi?%E5%A4%9A%E5%80%A4%E3%82%92%E5%A4%89%E6%95%B0%E3%81%AB%E4%BB%A3%E5%85%A5%E3%81%99%E3%82%8B
とりあえずそんな理解で合ってそうです。
concatenateは名前からしてもread-lineで読んだ文字列に括弧を足してそうですが、
構文がよくわかりませんね。
単純に最初の引数でこれから文字列を処理してもらうことを教えてるのでしょうか。
Common Lispで文字列をリストにしたい、リストを文字列にしたい、というときにconcatenate
そんな感じっぽいですね。リストの連結もできるとは。
理解するために使ってみます。

CL-USER> (concatenate 'string "俺は" "文字列" "だ")
"俺は文字列だ"
CL-USER> (concatenate 'list '(私は リスト) '(なんで すよ))
(私は リスト なんで すよ)
CL-USER> (concatenate 'list "俺は" "文字列" "だった")
(#\U4FFA #\HIRAGANA_LETTER_HA #\U6587 #\U5B57 #\U5217 #\HIRAGANA_LETTER_DA
 #\HIRAGANA_LETTER_SMALL_TU #\HIRAGANA_LETTER_TA)
CL-USER> 
; Evaluation aborted on #<SB-INT:SIMPLE-READER-ERROR "Nothing appears after . in list." {1003FC93C3}>.
CL-USER> (concatenate 'string '(i was) '(list yesterday))
; Evaluation aborted on #<TYPE-ERROR expected-type: CHARACTER datum: I>.
CL-USER> (concatenate 'string '(i was list))
; Evaluation aborted on #<TYPE-ERROR expected-type: CHARACTER datum: I>.
CL-USER> (concatenate 'string '(#\U4FFA #\HIRAGANA_LETTER_HA #\U6587 #\U5B57 #\U5217 #\HIRAGANA_LETTER_DA
 #\HIRAGANA_LETTER_SMALL_TU #\HIRAGANA_LETTER_TA))
"俺は文字列だった"
CL-USER>

色々使いかたができそうですね。リストを文字列にするには全要素をcharacterに
しなきゃいけないみたいですが、エラーをよく見ずいっぱい怒られました。
あんまりリスト→文字列を使うことはなさそうです。めんどくさいから。

え〜と、今の所read-lineで読んだ文字列の両側に括弧を足して、
それをread-from-stringで読んでダブルクオートを外したものをcmdにletで格納してます。
そのあとはquoteと引数をリストにするquote-it関数を定義して、
それをcmdのcdr全てに適用、carとquote-itを適用したcdrをconsすると。

今までシンボルにするのに使ってきた'は、quoteの省略形だったようです。
なるほどなあ…疲れた…。
しかし、これでもう大丈夫です。

でも、今日は早く寝たいのでここまでです。
一日一関数定義は気長すぎですが、まあ続けることが大事ですよね、うんうん。
おやすみなさい。