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

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

Common Lispを頑張る(5)

今日もLispをやっていきます。
C++をなぜやらないかと言うと参考書が分厚く持ち運びがダルいからです。
(退社したあとカフェで勉強してます。)
ともかく「Land of Lisp」第5章、やります。

悲しいことにコンピュータとテキストは相性が良いものではない、
しかしヒトとコンピュータがやり取りするにはテキストを使わざるを得ない…
と、悲しい導入で始まる5章ではテキストの扱い方を教えてくれるそうです。

次に、テキストゲーム作成を通してテキスト操作に慣れようというわけで
どんなゲームをつくるか、どんな場所が舞台なのかが説明されます。
丸パクリするのはアレなので、せめて設定だけでも変えます。

■設定(読み飛ばして大丈夫です。)
・ファンタジーのテキストRPGにします。
・主人公は「はじまりの村」に住む。
・「はじまりの村」は「活気ある街」に街道を通っていける。
・「活気ある街」の中には「王様の城」があり、橋を通っていける。
とりあえずこんなもんですかね。

さて、これから作るゲームにはいくつかの基本的な動作が必要です。
・周囲を見渡す。
・別の場所へ移動する。
・オブジェクトを拾う。
・オブジェクトでなにかする。
まずこの章では上の3つを実装できるようにしてくれるそうです。
最後のは17章で完成するらしいので、先は長いですね…。

まずは場所を見渡せるようにします。

連結リスト

周りを見渡したときに得られるべき情報は3つです。
・周りの景色。
・別の場所に繋がる道。
・手に取り操作できるオブジェクト。
まずは場所の景色を、連結リストとやらを使って書きます。

CL-USER> (defparameter *node* '((はじまりの村 (あなたははじまりの村にいる。
					       どこからか牛の鳴き声が聞こえる。))
				(活気ある街 (あなたは活気ある街にいる。
					     大通りには露店が並んでいて、あちこちで交渉が行われている。))
				(王様の城 (あなたは王様の城にいる。
					    警備兵が退屈そうに欠伸をしている。))))
					       
*NODE*
CL-USER>

それっぽくなってるといいのですが、景色と場所を紐づけてみました。
まあ内容はどうでもいいんですが。
このグローバル変数*node*は、場所に紐付いた景色を探し出すための構造だと言えます。
こういった構造を連結リスト(association list, alist)と呼ぶそうです。

(defparameter alist (キー (キーに紐づくデータ)))

こんな感じで作ればいいのでしょうかね。

さて、ここにはどうやらもう一つポイントがあるようで、
それはキーにもデータにも文字列を使っていないことです。
なんでこうするのかというと、冒頭述べられたようにコンピュータが
テキスト操作を苦手としているため、元となるデータ構造をその言語が
得意な形(Lispならリストとシンボル)で持っておいたほうが楽、という考えのようです。

さて、場所の描写のリストはできたので、そこからデータを取り出して
実際に表示してくれる関数を作らねばなりません。
assocという、リストからキーを元に要素を抜き出す関数があるということでそれを使います。

CL-USER> (defun describe-location (location nodes)
	   (cadr (assoc location nodes)))
DESCRIBE-LOCATION
CL-USER> (describe-location 'はじまりの村 *node*)
(あなたははじまりの村にいる。 どこからか牛の鳴き声が聞こえる。)
CL-USER>

何故かわざわざ*node*を引数として受け取り使っています。
それさえなければ引数が減って楽なのに…。
これにはれっきとした(?)理由があり、この関数は
関数型プログラミング」で書かれているからだそうです。
関数は引数か関数内で宣言した変数しか参照せず、
値を返す以外の動作をしないというのが関数型のスタイルだとか。
まあ、周りに与える影響がない関数というのはいいことですよね。
自分ひとりで書くとしたら絶対*node*を参照していたので気をつけねば。

準クオート

さてさて、次はある場所から別の場所に移動するときの通り道を作ります。

CL-USER> (defparameter *edges* '((はじまりの村 (活気ある街 東 街道))
				 (活気ある街 (はじまりの村 西 街道)
				            (王様の城 街の中心 橋))
				 (王様の城 (活気ある街 城門 橋))))
*EDGES*
CL-USER> (defun describe-path (edge)
	   `(,(cadr edge)には,(caddr edge)があり、,(car edge)へと続いている。))
DESCRIBE-PATH
CL-USER> (describe-path '(活気ある街 東 街道))
(東 には 街道 があり、 活気ある街 へと続いている。)
CL-USER> 

関数describe-pathの中では、シンボルの中に処理を埋め込んでいます。
これを可能にするポイントは2つあって、
・シンボルをただのクオートではなくバッククオートで示すこと。
・埋め込みたい処理の前にコンマを置くこと。
これをすることで、データモードの中のある部分をコードモードで読ませられるようです。
これはアンクオートと呼ばれるものだそうです。

高階関数

先程のdescribe-pathは引数が多くて面倒なので、
場所と*edge*を与えたら全ての道を教えてくれるようにしたいです。
というわけで、紹介されたコードがこちらです。

CL-USER> (defun describe-paths (location edges)
	   (apply #'append (mapcar #'describe-path (cdr (assoc location edges)))))
DESCRIBE-PATHS
CL-USER> (describe-paths '活気ある街 *edges*)
(西 には 街道 があり、 はじまりの村 へと続いている。 街の中心 には 橋 があり、 王様の城 へと続いている。)
CL-USER>

…まいりました。何が起きているのかさっぱりです。
#'とかいう怪しい記号がありますね…。
参考書をちょっと時間をかけて読みます…。むむ…ふむふむ…。

ちょっとわかった気になってきました。
まずはmapcarです、こいつからかかるべきだったようです。

mapcarは高階関数と呼ばれる関数です。引数に他の関数とリストを受け取り、
リストの要素の一つ一つに関数を適用します。

CL-USER> (mapcar #'abs '(-1 0 -100 -8 -27))

(1 0 100 8 27)
CL-USER>

リスト内の全要素の絶対値を出してもらいました。
これは使いこなせれば便利そうですね。
この怪しい記号#'は、関数を値として扱う場合に、
「これは今は値として扱っているけど、本当は関数なんだよ」と
Lispにわかって貰うために必要なものだそうです。
Common Lispでは関数と変数の名前空間が違っているために、
関数と変数の名前の衝突があり得るため、このように明示してやる必要があるとか。
そもそも衝突させたくないですが。

つまり(mapcar #'describe-path (cdr (assoc location edges)))は、
渡された場所に紐付けられた経路を取り出し、describe-pathを適用していると。

CL-USER> (mapcar #'describe-path (cdr (assoc '活気ある街 *edges*)))
((西 には 街道 があり、 はじまりの村 へと続いている。) (街の中心 には 橋 があり、 王様の城 へと続いている。))
CL-USER>

しかしこのままではリストの中に2つのリストがある状態で出力されます。
これをどうにかしているのが、apply #'appendの部分でしょう。

append関数は複数のリストをまとめて一つのリストにしてくれるそうです。
しかし、appendには複数のリストを別々の引数として渡す必要があるといいます。

CL-USER> (append '(1 2) '(3 4) '(5 6))
(1 2 3 4 5 6)
CL-USER> (append '((1 2) (3 4) (5 6)))
((1 2) (3 4) (5 6))
CL-USER>

1つ目の実行例が正しいappendの使いかたですね。
今回は2つ目の様な感じで渡してしまうのでこのままでは何もしてもらえません。

これを解決するのがapply関数です。
apply関数に関数とリストを渡すと、そのリストの各要素を引数として
関数を実行したかのような動作をしてくれるそうです。

CL-USER> (apply #'append '((1 2) (3 4) (5 6)))
(1 2 3 4 5 6)
CL-USER>

…おお!素晴らしい関数です!

今回通り道を描写するのに使ったような処理の構造(複雑なデータを複数のステップで受け渡していく処理)は
Lispでは典型的なものであり、高階関数もよく使われるそうです。
Lispの道を極めたければこれぐらいサラッと読めるようにならないと、
との言葉を文中に見つけ、胸にぐさっと刺さりました。

まだ途中ですが、今回はここまでとします。