星田さんの記事に対するコメント。
ところが場所を試しにgarden や atticにしてみるとエラーが。うーむ・・これは連想リストの書き方に問題があるな・・
連想リストの概要が知りたい場合には、既に入手してるわけだから実用Common Lispをまずは見てみよう。
実用Common Lisp第3章は極めて短い「ANSI Common Lisp リファレンス」になっている。ここで基礎的なLispの「機能」が紹介されている。
連想リストは69ページに概要が書いてると思う。「具体的な使い方」に関しては323ページに示されている。
このように、実用Common Lispは必ずしもリファレンスじゃないけど、「リファレンスのように」使う事が出来る。
しかもこの本は「プログラムの書き方を指南する」本なんで、場合によっては「詳細な使い方の例示」にお目にかかる事が出来ると思います。
続いて関数「walk」に入る。とりあえずCL用のコードに日本語訳を入れまして・・if next って? あとcar next? はぁ?nextにはリストが入ってるのか?とりあえずfind-ifが何を返すのかを調べないと・・
(if next はnextが偽じゃなかったらオッケイってだけと判明)
これ最初は誰でも「え?」とか思うのよ(笑)。
ANSI Common LispはNIL(あるいは'())じゃなければ全部真、Schemeは#fじゃなければ全部真、と言うのがこの時点までピンと来ないんだな。
> (if '(1 2 3 4)
5
'(6 7))
5
> (if "hoge"
"fuga"
"piyo")
"fuga"
> (if #(0 1 2)
#(3 4)
#(5 6 7))
'#(3 4)
> (if #\a
#\b
#\c)
#\b
> (if #f
"古池や蛙飛びこむ水の音"
"Full it care, cow was to become me is note.")
"Full it care, cow was to become me is note"
>
上の例を見てみれば分かると思うけど、Schemeではelse節が実行されてるのは最後の#fをifに与えたブツしかない。
つまり、Schemeでは#f以外は全部真だ、と言う事です。
同様に、ANSI Common Lispでは空リスト('())「だけ」が唯一存在する偽値であって、それ以外は全部真です。
ちょっとビックリするかもしれないけど、お陰でLispでは条件節の部分をやたらシンプルに記述する事が可能です。
プロシージャって何でしたっけw
値を返さない関数・・・いや、何か返してくれないとfindfの引数にならんのでは?まあ、関数とよく似たものなのか・・としておく、現時点では!
うん、この説明はコンピュータ・サイエンス的には正しい。
ただね、「コンピュータ・サイエンス」用語と「それぞれのプログラミング言語で定義されてる用語」が一致してるたぁ限らないんだわ(笑)。
一つ把握しておこう。プログラミングでの用語定義はかなりいい加減だ、と言う事を。
ハッキリ言うと、プログラミングに関して用語で混乱するのは、「プログラミングを学ぼうとする人が文系の場合」に限んないの。理系でも混乱する。
っつーか、数学とか一般に自然科学、って言われる理学系の分野だと用語はもっと明確に定義しててブレないんだよ。ところがプログラミングの分野だとメチャクチャ過ぎる、ってのが正直なトコだ。
これ、工学一般がそうなのかどうかは知りません。でも少なくともこの辺が証明してるのはプログラミングは理学ではない、って事です。
んで一般的定義はさておいて。実は用語的にはSchemeには関数がありません。SchemeにあるのはProcedure(手続き)だけ、です。
これはSchemeには関数がない、って言ってるんじゃなくって、一般的に言う関数がSchemeの仕様上ではProcedureと呼ばれてる、と言う意味です。メンドクセェだろ(笑)?
もう一回言うと、コンピュータ・サイエンス的には上の写真の通り、です。
ただ、一般的に言うと、プログラミング言語毎での呼び方や「機能の誤魔化し」っつーのがあって、僕が知ってる限り、マジで関数とProcedureをキチンと分けてユーザーが定義せなアカン言語って、ぶっちゃけPascalくらいしか知りませんね。
例えばC言語では全部「関数」と呼ぶんだけど、
/* 整数を「返す」関数の例 */int foo (int x) {return x;}
同じ関数でもvoid型、ってのを返す場合、コンピュータ・サイエンスで言うトコのProcedureとなります。
/* 何も返さない(return しない) */void foo (int x) {printf("%d\n", x);}
同様に、Pythonだとこれは関数になるけど、
# x をreturn するのでこれはコンピュータ・サイエンス上は関数def foo(x):return x
これはコンピュータ・サイエンス上はProcedure。
# PythonでのNoneを返す関数、と言うのはコンピュータ・サイエンス上はProcedureになるdef foo(x):print("{}".format(x))
一方、ANSI Common Lispの場合。ANSI Common Lispは含まれるどんな関数でも全部値を返すようになっている。
従って、コンピュータ・サイエンス的に見てもANSI Common Lispは関数型言語なんです(※1)。言い換えるとANSI Common LispにはProcedureが存在しない。
ところが厄介なのはSchemeだ。
- 仕様上、Schemeには関数は存在せずProcedureしか存在しない。
- SchemeのProcedureは値を返すProcedureと値を返さないProcedureがある。
まぁ、コンピュータ・サイエンス的には「何だこれは」と言う話になってくるんだけど、それはさておき、「ANSI Common Lispは必ず値を返す」けど「Schemeには値を返すProcedureと値を返さないProcedureがある」と言うのは押さえておこう。
コンピュータ・サイエンスな文脈では「ANSI Common LispにはProcedureはない」けど「Schemeには関数とProcedureがある」って事だ。
いずれにせよ、プログラミング言語を操る際、
「どれがコンピュータ・サイエンス用語でどれがプログラミング言語仕様でのローカル定義なんだろ。」
ってのは注意深くしとかないといけない。
メンド臭いね!
nextの中身がlistってのがどうしても納得できなくて中身を表示してみる(コードは例のカンニングサイトを見てしまった、とりあえず今だけ!)。えっ・・確かにリスト。けど、なんでfindでの返り値がこの形で返ってくるのか分からない・・
今、取り敢えずこんな状態なのかね?
#lang racket
(define *nodes* '((living-room (you are in the living-room.
a wizard is snoring loudly on the couch.))
(garden (you 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.))))
(define (describe-location location nodes)
(second (assq location nodes)))
(define *edges* '((living-room (garden west door)
(attic upstairs ladder))
(garden (living-room east door))
(attic (living-room downstairs ladder))))
(define (describe-path edge)
`(there is a ,(third edge) going ,(second edge) from here.))
(define (describe-paths location edges)
(apply append (map describe-path (cdr (assoc location edges)))))
(define *objects* '(whiskey bucket frog chain))
(define *object-locations* '((whiskey living-room)
(bucket living-room)
(chain garden)
(frog garden)))
(define (objects-at loc objs obj-loc)
(letrec ((is-at? (lambda (obj)
(eq? (second (assq obj obj-loc)) loc))))
(filter is-at? objs)))
(define (describe-objects loc objs obj-loc)
(letrec ((describe-obj (lambda (obj)
`(you see a ,obj on the floor.))))
(apply append (map describe-obj (objects-at loc objs obj-loc)))))
(define *location* 'living-room)
(define (look)
(append (describe-location *location* *nodes*)
(describe-paths *location* *edges*)
(describe-objects *location* *objects* *object-locations*)))
(define (walk direction)
(letrec ((correct-way? (lambda (edge)
(eq? (second edge) direction))))
(let ((next (findf correct-way? (cdr (assq *location* *edges*)))))
(if next
(begin (set! *location* (car next))
(look))
'(you cannot go that way.)))))
まぁ、これだと仮定して。
実のこと言うとfindfがどーの、じゃなくって連想リストの方の問題なんだよな。
つまり、今対象にしてるデータ、つまり連想リストはこれでしょう。
(define *edges* '((living-room (garden west door)
(attic upstairs ladder))
(garden (living-room east door))
(attic (living-room downstairs ladder))))
まずね。ドット対が〜、とか書いてたから、これ、一回こう書き換えてみた、って事なのかな。
(define *edges* '((living-room (garden west door)
(attic upstairs ladder))
(garden . (living-room east door))
(attic . (living-room downstairs ladder))))
写真によると?
まぁ、実験としてはいいんですが・・・・・・。
まず1つ目。
これだけ実行してみれば分かると思うんだけど。
> '(garden . (living-room east door))
'(garden living-room east door)
> '(attic . (living-room downstairs ladder))
'(attic living-room downstairs ladder)
>
実は'(element . lst)と言うのは(cons element lst)と同じなんです。
言い換えると、第1要素がリストだった場合、ドット対はあまり意味がない・・・っつーか「思いがけない」結果になるかもしんない。
(define *edges* '((living-room (garden west door)
(attic upstairs ladder))
(garden living-room east door))
(attic living-room downstairs ladder))))
この状態になればマズいわけでしょう。
元々、キーが「現在地」を表し、「行ける場所」をリストとして表現してるわけだから。
living-roomは二箇所に行けるがgardenとatticはそれぞれ一箇所にしか行けない。
その均衡がドット対のせいで崩れちゃダメだよね。
そして「データ形式は統一形式としておかなきゃならない」と言う原則にも反している。
そして、星田さんがやってるようにwalkに和訳付けてみようか。
(define (walk direction)
(letrec ((correct-way? (lambda (edge) ; 局所関数(述語) correct-way?
; edgeのsecondとdirectionがあってるか
(eq? (second edge) direction))))
; 局所変数 next に correct-way? の条件を満たすものを束縛する
(let ((next (findf correct-way? (cdr (assq *location* *edges*)))))
(if next
(begin (set! *location* (car next))
(look))
'(you cannot go that way.)))))
さて、そうすると、最初の*location*は'living-roomだよな。
したがって、findfの第二引数は(cdr (assq *location* *edges*)なので
> (cdr (assq 'living-room *edges*))
'((garden west door) (attic upstairs ladder))
>
と言う事だ。
つまり、walk関数に引数directionが与えられた場合・・・ゲームの制限上はwestかupstairs、って事なんだけど、結局findfでは
- directionがwestだった場合 -> (garden west door)が返り値(westはsecondだ!)
- directionがupstairsだった場合 -> (attic upstairs ladder)が返り値(upstairsはsecondだ!)
になる。だからset! *location*には(car next)をハメないとならない。
そして忘れてるかもしれないけど、findfはmapと同様に、第二引数にリストを取る高階関数となる。
そして、今問題になってるのは、引数は単なるリストではなくってリストのリストになってるんだよな。
つまり、ローカル関数っつーか、ローカル述語であるcorrect-way?ってのは内側の要素であるリストの第二要素がdirectionと等しいのか調べてるわけで、外側のリストの第二要素を調べてるわけじゃないんだ。
'((garden west door) (attic upstairs ladder));; second っつってもこの2番目のリスト((attic upstairs ladder))じゃない'((garden west door) (attic upstairs ladder))↑ ↑;; 各要素の「2番目」がどうなのか調べている
でもfindfは「要素」を返すわけで、「要素の2番目が目的のシンボルと一致した場合」その要素自体、つまり今回のケースだと「目的のシンボルを含んでた要素(リスト)」をそのまま返すわけ。
お分かりでしょうか?
※1: 若干安直な結論である。
ただし、何度も言ってる通り、本流のLispはまさに「関数型言語」を目的として開発されていて、Haskellが「純粋な」関数型言語、と言うモノを定義したのはもっと後の話である(数学的にどうなのか、と言う研究が整備されるまで時間がかかったのだ)。
いずれにせよ、普通のプログラミング言語でProcedureにあたるモノまで「無理矢理何としてでも値を返すように」ANSI Common Lispは設計されている。