見出し画像

Retro-gaming and so on

ババ抜きを作ろう その2

5. メッセージ分離方式(承前)

次は捨てられたカードを表示する関数、showDiscardCardsを作る。

;;; 捨てられたカードを表示する文字列

(define (showDiscardCards id cards)
 (if (null? cards)
  ""
  (
format (cdr (assq 'showDiscardCards *messages*))
      id (getCardsStr cards))))

これも、カードを引いたからと言って毎回毎回ペアが成立してカードを捨てられるとは限らない。
従って、情報としては、ペアが成立してカードが捨てられる場合とそうじゃない場合があり、後者の場合はlset-differenceの仕様により空リストになっている。

> (lset-difference eq? '(a b c) '(a b c)) ;; ペアが成立しなかった場合に相当する
'()
>

要するに、ペアが成立せず、何もカードが捨てられなかった場合は整形した文字列自体が不要なので、空の文字列を返すようになっている。
なお、仮引数で使われてるidはプレイヤーの「番号」を意図してる。

次は誰が誰から、何のカードを引いたか、を表示する文字列を生成する関数を書く。

;;; カードの交換状況を表示する文字列

(define (showDrawCard id1 id2 card)
 (format (cdr (assq 'showDrawCard *messages*))
    id1 id2 (getCardStr card)))

これは前者二個と比べると大した工夫は要らない。
そしてゲーム終了時に表示する文字列を生成する関数はこれ、だ。

;;; ゲームの結果を表示する文字列

(define (showResult id)
 (format (cdr (assq 'showResult *messages*)) id))

これも取り立ててややこしくないし、まだ作ってないgetCardStrgetCardsStrも使ってないので、テストも簡単だ。単にidに適当な番号を与えるだけ、でテストできる。

> (showResult 0)
"プレイヤー0の負けです..."
>

あとは、今まで書かなかったgetCardStrgetCardsStrを作るだけ、である。

;;; Cardオブジェクトのリストを文字列に変換

(define (getCardsStr cards)
 (apply string-append
   (map getCardStr cards)))

;;; Cardオブジェクトを文字列に変換

(define (getCardStr card)
 (format (cdr (assq 'getCardStr *messages*))
    (Card-suit card) (Card-number card)))

基本は後者の関数であるgetCardStr である。これはカードの構造体を一枚受け取って出力形式の文字列に変換して返す。

> (getCardStr (Card "J" "0"))
"[J:0]"
>

これを利用すると、複数のカードを受け取って全部を出力形式の文字列に変換した文字列を返す関数getCardsStrを書くのは簡単だ。単純にはカードのリストを受け取ってgetCardStrmapすれば良い。

> (getCardsStr `(,(Card "H" "3") ,(Card "J" "0") ,(Card "S" "10") ,(Card "S" "J")))
"[H:3][J:0][S:10][S:J]"
>

では動作テストをまとめてやってみよう。

> (showCards `((,(Card "H" "3")) (,(Card "J" "0")) (,(Card "S" "10")) (,(Card "S" "J"))))
"プレイヤー0のカード: [H:3]\nプレイヤー1のカード: 1枚\nプレイヤー2のカード: 1枚\nプレイヤー3のカード: 1枚\n"

> (showDiscardCards 0 `(,(Card "H" "3") ,(Card "S" "3")))
"プレイヤー0が捨てたカード: [H:3][S:3]\n"
>

> (showDrawCard 0 1 (Card "J" "0"))
"プレイヤー0 -> プレイヤー 1 : [J:0]\n"
>

これで出力関数printに必要な文字列の整形は終わった。
次はカードデッキを生成する関数を作ろう。

6. トランプのカードを作る

デッキを作る関数は以下のように定義する。

;;; トランプのカードを作成する

(define (make-deck)
 (cons (Card "J" "0")
   (append-map (lambda (suit)
        (map (lambda (number)
           (Card (cond ((zero? suit) "D")
               ((= suit 1) "H")
               ((= suit 2) "S")
               (else "C"))
              (cond ((zero? number) "A")
               ((= number 10) "J")
               ((= number 11) "Q")
               ((= number 12) "K")
               (else (number->string
                  (+ 1 number))))))
         (range 13))) (range 4))))

発想は簡単だろう。単純にはトランプの構造体に数値を当て嵌めるようなループをmappingを用いて行ってるだけ、だが、その際にCard-suitにはダイヤ、ハート、スペース、クラブを表す文字を条件により当てはめ、そしてCard-numberには0の時A、10の時J、11の時Q、12の時にはKを当てはめ、それ以外の時は数値に+1したものを文字列に変換して当てはめている。
最後にジョーカーをconsすればトランプのセットが一式出来上がる。

> (make-deck)
(list
 (Card "J" "0")
 (Card "D" "A")
 (Card "D" "2")
 (Card "D" "3")
 (Card "D" "4")
 (Card "D" "5")
 (Card "D" "6")
 (Card "D" "7")
 (Card "D" "8")
 (Card "D" "9")
 (Card "D" "10")
 (Card "D" "J")
 (Card "D" "Q")
 (Card "D" "K")
 (Card "H" "A")
 (Card "H" "2")
 (Card "H" "3")
 (Card "H" "4")
 (Card "H" "5")
 (Card "H" "6")
 (Card "H" "7")
 (Card "H" "8")
 (Card "H" "9")
 (Card "H" "10")
 (Card "H" "J")
 (Card "H" "Q")
 (Card "H" "K")
 (Card "S" "A")
 (Card "S" "2")
 (Card "S" "3")
 (Card "S" "4")
 (Card "S" "5")
 (Card "S" "6")
 (Card "S" "7")
 (Card "S" "8")
 (Card "S" "9")
 (Card "S" "10")
 (Card "S" "J")
 (Card "S" "Q")
 (Card "S" "K")
 (Card "C" "A")
 (Card "C" "2")
 (Card "C" "3")
 (Card "C" "4")
 (Card "C" "5")
 (Card "C" "6")
 (Card "C" "7")
 (Card "C" "8")
 (Card "C" "9")
 (Card "C" "10")
 (Card "C" "J")
 (Card "C" "Q")
 (Card "C" "K"))
>

これでゲームの初期化に纏わる関数は書き終わった。
次はREPLで持ち回す環境を定義しよう。

7. 環境用構造体を作る

今回は別にGUI版を作る意図はないのだが、world構造体、と言う名前が面白いので(笑)、継続してこの名前を用いよう。
world構造体を取り敢えず次のように定義してみる。

;;; 環境

(struct world (card clist discarded dpair id1 id2 players)
 #:transparent)

環境用のworld構造体は7つのスロットを持つように設計した。
各スロット(Racketではフィールド、と呼んでるが)の役割は以下の通りである。

  • card: あるプレイヤーが引いたカードが何か、を示すスロット。出力関数用である。
  • clist: ババ抜きのカウンター(id)の参照用リストである。不変。プレイヤー陣は巡回するので、それを綺麗に行う為のトリックを司る。後述。
  • discarded: ペアが成立した時、捨てられたカードが何なのか保持するスロット。出力関数用である。
  • dpair: ドットリスト。world構造体のid1id2はそれぞれ「カードを引くプレイヤー」と「カードが引かれるプレイヤー」の番号を意味してるが、world構造体がevalによって計算された時点でid1id2は「次のプレイヤーの組」へと更新されてしまう。従って、出力用に「今回のid1id2だった値」を保持しておかないとならない。(car dpair)が今回のid1(cdr dpair)が今回のid2を意味する。
  • id1: カードを引く側、の番号である。具体的にはplayersリストからplayer1を引っ張ってくる為の参照番号。
  • id2: カードを引かれる側、の番号である。具体的にはplayersリストからplayer2を引っ張ってくる為の参照番号。
  • players: カードのリストのリストであり、各プレイヤーが「何の手札を保持してるか」を保持してるリストである。(list-ref players id)が具体的なプレイヤーを示す事となる。
各種スロットの役割を押さえておいて、いよいよババ抜きの心臓部、evalを書いていく。

8. eval を作る

前回書いた通り、今回のevalは言語インタプリタのような複雑さは持ってない。むしろ単純なスクリプトに極めて近いカタチ、である。
が、ちょっと見た目は厳しいかもしれない。
いずれにせよ、紹介しよう。

;;; eval

(define (world-go exp w)
 ;;; world 構造体を組み立て直す材料は4つしか必要ない
 (let ((clist (world-clist w))
   (id1 (world-id1 w))
   (id2 (world-id2 w))
   (players (world-players w)))
  ;;; カードを引く側、引かれる側、を定義する
  (let ((player1 (list-ref players id1))
    (player2 (list-ref players id2)))
   ;;; player1 が player2 からカードを引く
   (let-values (((card player1 player2)
        (drawCard player1 player2 exp)))
    ;;; player1 がペアになったカードを捨てる
    (let-values (((discarded player1)
         (discardPair player1)))
     ;;; players リストを更新する
     (let ((players (insert id1 player1
            (insert id2 player2 players))))
      ;;; id1 を更新する
      (let ((new-id1 (next-id clist (add1 id1) players)))
       ;;; 新しい id1 を利用して id2 を計算する
       (let ((new-id2 (next-id clist (add1 new-id1) players)))
        ;;; 新しい world 構造体を生成し返り値とする
        (world card clist discarded
          ;;; 出力用に更新前の id1 と id2 を保持する
          (cons id1 id2)
          new-id1 ;; 更新後の id1
          new-id2 ;; 更新後の id2
          ;;; 新しい players
          players)))))))))

まず、入力情報を受け渡されるexpは原則、ある範囲の整数しか受け取らない。特にプログラミング言語やアドベンチャーゲームエンジンのようなコマンドで処理を分ける必要がないので、その辺はシンプルに仕上がってる。
処理の流れは次のようになっている。

  1. カードを引くplayer1、カードを引かれるplayer2を定義する。
  2. player1player2からカードをexpに従って一枚引き、引いたカードの情報をcardとして保持し、player1(カードが一枚多くなる)とplayer2(カードが一枚減る)を改めて算出する。
  3. player1はカードを引いた事により、ペアが出来たかどうかを調べ、それを捨てる。捨てたカードは変数discardedとして保持され、またもやplayer1を再計算する。
  4. player1及びplayer2が再計算された為、playerのリストplayersも更新する必要がある。新しいplayer1は旧いplayer1の位置に、新しいplayer2は旧いplayer2の位置に挿入される。
  5. id1を更新して、次に「カードを引くヤツ」の番号を算出する。
  6. 新しいid1(new-id1)の次から新しいid2(new-id2)を探す。
  7. 新しく算出したcarddiscardednew-id1new-id2players及び旧いid1id2を利用して新しくworld構造体を生成してそれを返り値とする。
と、基本7ステップの作業となる。
なお、carddiscardeddpairの3つは「出力用」なんで、前回の値がどーだったのか、ってぇのはぶっちゃけ関係がないのだ。だから最初の「構造体分解」で思いっきり無視してるわけである。ハッキリ言うと捨て去って構わない値なのだ(出力が終わればお前ら用済み、ってヤツである)。
ステップ的な流れ作業を見ると、実際は取り立てて難しい事をやってるわけじゃあない、ってのが分かるだろう。変数と変数がどういう流れで繋がってるのか把握出来れば、むしろ「易しいプログラムなんだ」って分かるんじゃないか。
しかし、難しい事をやってるわけじゃあないのに、見た目ちと厳しいのは事実である。原因はハッキリしてる。letlet-valuesの多用だ。それらが段を組んでるのだ。
実の事を言うと、同様のプログラムを別の言語で書いたらこんなに厳しくは見えない。ある種のLisp故の特殊性がこういう「見た目の厳しさ」を醸し出してるのだ。

9. Lispの泣き所

Lispが一般に嫌われるのは、「異様に括弧が多い為」だと言われる。
しかし、実はそれは本質的な部分じゃない。
Lispが嫌われる本当の理由、ってのは「書いてるプログラムのネスト(入れ子)がどんどん深くなる」と言うトコなんだ。そしてLispの括弧はぶっちゃけネスト、あるいはインデントレベルの深さを象徴するサインなのである。
だからプログラマは直感的に「Lispは括弧が多くて・・・」と言うのである。実際これが意味するのは、プログラマは一般に「深いネストは好まない」と言う事だ。
そして最悪な事にLispはネストを許容する。いや、逆に関数やマクロが形成するS式のネストこそがLispのプログラム構造を決定してるので、だからこそ一般的なプログラマはLispを嫌うのである。
上のevalでは、let及びlet-valuesが現れる度にネストが深くなっていってるのが視覚的に分かるだろう。だから「何か嫌だな」と思うわけだ。
いや、もっと言っちゃうと「あれ、俺って今、取り返しの付かないモノを書いてない?」と疑心暗鬼に襲われるのだ(笑)。こんなにネストが深くなってるのに、最深部から無事戻ってこれるのだろうか、と(笑)。まさに深遠に潜っていってるような不安と憂鬱さに襲われるのである。
例えば、同様のプログラムをPythonで書いてみたら、と言う思考実験を行ってみよう。恐らく次の様なプログラムになるだろう。

def world-go(exp, w):
 clist = w.clist
 id1 = w.id1
 id2 = w.id2
 players = w.players
 player1 = players[id1]
 player2 = players[id2]
 card, player1, player2 = drawCard(player1, player2, exp)
 discarded, player1 = discardPair(player1)
 players = insert(id1, player1, (insert id2, player2, players))
 new_id1 = next_id(clist, id1 + 1, players)
 new_id2 = next_id(clist, new_id1 + 1, players)
 return world(card, clist, discarded, [id1, id2], new_id1, new_id2, players)

恐らく「え?」と思うだろう。むしろ「バカっぽい」プログラムにさえ見える筈だ(笑)。代入だらけで、しかもネストが生じていない。非常に単純なフラットな印象のプログラムである。
それにしても、全くLispと同じ事をやってる筈なのに印象が全然違う事に驚くだろう。そして同じ事をやってる筈なのにLispが厳しく見える事の方が問題、なのだ。
この秘密が何なのか、と言うのは先程言った通り、本質的にLispはネスト絡みでプログラムを生成する言語だ、と言うことである。
しかも、普段Lispでプログラムする際には、我々は引数の「評価順序」をあまり気にしない。単にどの変数とどの変数が関連してるのか、あるいは同じなのか、と言うのを気にしてるに過ぎない。
しかし、こういうlet、及びlet-values絡みになると、その辺のLispの「おおらかな性質」がハッキリ言って裏目に出る。
通常、Pythonのようなフツーのプログラミング言語だと、代入は代入であって、変数の破壊的変更はあまり気にせずにプログラムするようになっている。
一方、Lispプログラミングではそのテの破壊的変更はあまりしない。しかも、構造上、「引数の評価順序」はあるが、一般的な意味での「プログラムの逐次実行」と言う辺りではLispは「我関せず」と言った態度なのだ。
どういう事だろうか。Lispには極論リストしか存在しない。プログラム全部がリストで構成されている。
そしてあるリストの第1引数は、そのリストの例えば第2引数から「参照される」と言う行為を表層的には禁止してるのだ。リストの第2引数から第1引数を参照する事を前方参照、と呼ぶ。
PythonとLispの「構文上の違い」と言うのはあるが、Lisp的に言うとPythonは記述したプログラム本体内での引数の前方参照を許してる、のだ(※1)。もちろんシステム的には全く違うわけだが、「Lisp的に解釈すると」そういう事を許容してる言語、と言う事になる。
具体的に言おう。例えば次のようなletの使い方をするとLispではエラーとなる。

> (let ((x 1) (y (+ x 1))) (list y x))
. . x: undefined;
cannot reference an identifier before its definition
>

yで「その直前に定義された」xは利用出来ない。つまり前方参照がまさしく禁止されている、のである。
結局、上のようなプログラムを書こうとしたら、yを新たにletで束縛し直さないとならない。

> (let ((x 1)) (let ((y (+ x 1))) (list y x)))
'(2 1)
>

と言う事は、何か前にletで定義した変数を利用して・・・って考えるとどんどんletを重ね書きしていかないとならない。そしてletを重ね書きしていくとネストがどんどん深くなっていく・・・。そういう悩みどころがLispにはあるんだな。
一方、Pythonならこんなカンジになるだろう。

>>> x = 1
>>> y = x + 1
>>> [y, x]
[2, 1]
>>>

Lispよりもシンプルだ。これもLisp的な観点では「前方参照」が成立してるから、である。
んで、実の事を言うと、前方参照を許す版のletも存在する。let*がそれだ。
そして多値のlet-valuesの前方参照可能版のlet*-valuesと言うマクロも存在する。
それらを用いてworld-goを書き換えると次のようになるだろう。

;;; let* と let*-values を用いた eval

(define (world-go exp w)
 (let* ((clist (world-clist w))
   (id1 (world-id1 w))
   (id2 (world-id2 w))
   (players (world-players w))
   (player1 (list-ref players id1))
   (player2 (list-ref players id2)))
  (let*-values (((card player1 player2)
        (drawCard player1 player2 exp))
        ((discarded player1) (discardPair player1)))
   (let* ((players (insert id1 player1
           (insert id2 player2 players)))
      (new-id1 (next-id clist (add1 id1) players))
      (new-id2 (next-id clist (add1 new-id1) players)))
    (world card clist discarded
      (cons id1 id2) new-id1 new-id2 players)))))

どうだろう。依然とネストがあるにはあるが、以前の版程は酷くない。
しかし、let内に「どういった順序で変数を置くか」と言うのは、前方参照が禁止されてるが故に自由だった。そこで定義された変数を別の変数から参照するのなら新しくletで束縛すれば良かった。いわば、前方参照が禁止されてたが故に、変数を置く順序は自由だったのである。
一方、let*let*-valuesの場合、前方参照が生じるが故に「変数を置く順序」をキチンと考えなければならない。言わば前方参照と言う自由と引き換えに「変数を置く順序はテキトーで良い」と言う自由を手放したカタチとなる。
つまり、Lispでは普段はあまり意識しない「逐次実行」が出てくるのである。
どっちのスタイルで書いても良いたぁ思う。所詮はスタイルだから、ね。
なお、ポール・グレアムは次のような事を書いている。

以下のオペレータは税金がかかっているつもりで扱うことだ:

set setq setf psetf psetq incf decf push pop pushnew
rplaca rplacd rotatef shiftf remf remprop remhash

あとlet*もそうだ. この中に命令的プログラムが潜んでいることがしばしばある.これらのオペレータに税金がかかっているつもりになるのは, よいLispのプログラミング・スタイルへ向かう手助けとして勧めただけで, それがよいスタイルの基準なのではない. しかし,それだけでもずいぶん進歩できるだろう. 

いずれにせよ、Lispの「逐次実行なんざカンケーねーよ」と言う自由度を重視するか、あるいは書いたソースコードの見た目の良さを重視するか、と言うのはLisp上でのなかなか悩ましい問題である(※2)。

さて、悩みは悩みでほっておいて(これはスタイル選択なので、僕の問題じゃなくて貴方の問題、となる)、次はevalで使った補助関数を作っていこう。

10. evalの補助関数

まずは狙った場所に要素を突っ込む関数insertを設計する。これは、world-go内でplayersリストを更新する為に使われる。
つまり、例えばplayer1が更新された際に、player1があった場所でplayer1を差し替えるのだ。
これは既にお馴染みになった感のある、split-at関数を利用して記述する。リストを任意の位置で二つに分割するのだ。

(define (insert id player players)
 (let-values (((head tail) (split-at players id)))
  (append head (cons player (cdr tail)))))

任意の位置で分割されたリストをheadtailと名付け、(cdr tail)playerconsした後headとまたもや連結する。
ある意味お馴染みのパターンだし、「カードを引く」関数(drawCard)でも既に似たような事はやっている。
これはLispプログラミングでは頻出パターンなので良く覚えておこう。

さて、次に関数new-idを設計する。world-go内でplayer1が他のプレイヤーからカードを引いて、成立したペアを捨てたあと、次に「カードを引く」プレイヤーがplayersリストの何番目にいるか検索する関数である。
ところで、world-go内の該当箇所を見ると次のように書かれている。

(next-id clist (add1 id1) players)

直感的に見ると、

「あれ、REPLがループ構造になってるなら、これだとid1にどんどん1が足されていかないか・・・・・・?」

と不安になるかもしんない。
もちろんそんな事はないし、例えばプレイヤーが4人いたとしたら、4番目のプレイヤーが誰からカードを引くのか、と言うと当然「存在しない5番目のプレイヤーから」ではなく、最初のプレイヤーから、になる。
つまり、idってのはこの場合、循環構造になってなければならない、と言う事だ。そしてその循環構造に則って次のidを返すのがnext-id関数なのである。

ところで、別に難しいプログラミング、ってワケでは必ずしもないが、このように

  • 最初のプレイヤー -> 次のプレイヤー -> ... -> 最後のプレイヤー -> 最初のプレイヤー…
と言うのを、例えば条件分岐なぞを使って書くのは割と煩わしかったりする。
しかし、Lispに於いてはとある指標を使ってこの循環構造を簡単に表現が可能だ。
その指標の名前を循環リスト(Circular List)と呼ぶ。今回、SRFI-1から引っこ抜いてきたライブラリ関数(circular-list)はこの循環リストを作成するために使うのだ。
その前に循環リストをちと説明しよう。
通常、Lispでのリストは次のような構造になっている。例えば'(1 2 3 4)と言うリストだと


と言う構造だ。
Lispのリストはメモリの一部分を二個合わせたモノを基本とし、これをコンスセルとかペアとか呼んだりする。そして向かって左側をcar、右側をcdrと呼称するのだ。
このコンスセルはメモリの一部ではあるが、中に現物を仕入れない。代わりにポインタと呼ばれるものを持っている。上の図では矢印がそのポインタである(※3)。
つまり、実はリスト'(1 2 3 4)では1、2、3、4と言う実体をこのリストは抱えていない。1、2、3、4と言う実体はメモリ上のどっか別のトコにあるのである。そしてそれがどこにあるのか、は通常、神のみぞ知る、ならぬOSのみぞ知る、だ。
いずれにせよ、Lispのリストは、表現的には「リストが何かを抱えてる」ように見えるが、実際はその「何か」はどっか別のトコロにあるのである。
そしてより重要なのは、通常、リストの終端はNILと呼ばれるトコを「指して」いる。NILもメモリ上のどっか、OSによって決められた場所である。Lisp上では空リスト、っちゅう事になってる。
ところで、今「通常は」と書いた。ところが、次のような不可思議なリストもLispでは存在するのである。


このケースの場合、4番目のコンスセルのcdrが指すポインタがリストの先頭を指している。こういうリストを循環リスト、と呼ぶのだ。
これはもちろん歪なリストだし(※4)、Lisp入門的な書物でチラッと紹介される程度のトリビアルな存在だ。なんせリストに終端がないんでフツーのリストの様に使えず、例えば「長さを測る」なんて事も出来ない。グルグルグルグル自らを永久に循環するだけ、だ。
つまり、コンピュータサイエンス的な意味での「存在の面白さ」を語る事は可能なんだけど、実用性は殆ど無いのだ。
ところがこういう面白い性質がある。
例えばフツーのリスト'(1 2 3 4)を考えよう。このリストの5番目の要素(と言うか0から数えれば4番目の要素だが)をlist-refで返そうと思えば当然エラーが起きる。

> (list-ref '(1 2 3 4) 4)
. . list-ref: index too large for list
index: 4
in: '(1 2 3 4)
>

ところが循環リストだとこうなる。

> (list-ref (circular-list 1 2 3 4) 4)
1
>

分かるだろうか?この循環リストの4番目の要素は1、なのである。
同様に5番目の要素を見てみよう。

> (list-ref (circular-list 1 2 3 4) 5)
2
>

今度はこの循環リストの5番目の要素は2、だと来たもんだ。
お分かりだろうか?循環リストは循環してるから、こそ一見リストの長さを超えた要素番号を指定しても全くエラーを起こさないのだ。しかも要素をキチンと返してくるし。
これは利用できるだろう。何故ならババ抜きもメンバー間での行動順序が循環してるから、である。
world構造体のclistスロットはこの循環リストを保存する為のスロットである。
そしてid計算上の参照先、としてコイツを使うわけだ。そうすれば余計な条件分岐を書かずにスマートにメンバー間の行動順序を循環させる事ができる。

まずは適当な数を与えられたら循環リストを生成するユーティリティ、make-clistを作成する。

(define (make-clist n)
 (apply circular-list (range n)))

動作をテストしてみよう。nに4を与えてみる。

> (make-clist 4)
#0='(0 1 2 3 . #0#)
>

なんかおかしな表現が返ってくるが、これが良く使われる循環リストの表現、である。
実は循環リストは再三書くが長さを持たないし、構造はグルグル回ってて終端を持たない。
一方、原理的には「出力関数」は要素を1個1個受け取りつつ表示する・・・この出力関数と循環リストの組み合わせは一体どうなるのだろう。
そう、フツーのリストのように表示しようとすると無限ループに陥るのだ。
もう一回言うけど、循環リストは終端要素を持たない。結果、出力関数が循環リストの要素を1個1個手繰って行ったとしても永久に終わりに到達する事がないのである。
この無限ループを避ける為に、Lisp処理系の出力関数は、循環リストを受け取った時に「循環リスト」を判別し、その出力形式を通常のリスト表示プロセスから変更するのである(※5)。

さて、循環リストを生成出来れば、「次のカードの引き手」等のプレイヤー番号を選出する関数、next-idを作るのは簡単である。

(define (next-id clist id players)
 (let ((pos (list-ref clist id)))
  (let ((player (list-ref players pos)))
   (if (null? player)
    (next-id clist (add1 pos) players)
    pos))))

ポイントは2つ。
1つ目は、どんなid(要するに例えばplayersリストの長さを超える整数)が与えられたとしても、playersリストの要素数と同数の要素数を持った循環リストを参照させる事で、勝手にplayersリストの長さに納まる整数に変換されてしまう、と言うこと(これがpos変数である)。それを使って(list-ref players pos)すれば決してエラーにならないと言う事。
もう一つはplayerと言うリストが空リストだ、と言う事は何抜けかは知らんが、とっくに手札を消費してゲームを抜けてる、と言う事。そのプレイヤーのidを採用してもしょーがないので、その場合は再帰して「次の候補者を探す」事。これが2つ目のポイントである。

さて、これで一応world-goは完全に動作するようになった。
次は出力用関数であるprintを作ろう。

11. printを作る

printを作る、って意気込んではみるが、実際のトコ、材料は既に「メッセージ分離方式」を利用して作ってる。あとやるこたぁ、基本的に「メッセージ分離方式」で作成した文字列整形関数達を結合してdisplayするだけ、である。
まずは完成形を見てもらおう。

;;; print

(define (print w)
 (let ((card (world-card w))
   (discarded (world-discarded w))
   (dpair (world-dpair w))
   (players (world-players w)))
  (let ((head (car dpair)) (tail (cdr dpair)))
   (display
    (string-append
     (showDrawCard tail head card)
     (showDiscardCards head discarded)
     (showCards players)))))
  w)

まず、キッカリと押さえておかなければならないのは、world構造体のスロットの中で明らかに出力関数用の情報がどれだったか、と言うことである。
具体的には

  • card: 現時点でのplayer1が引いたカード
  • discarded: 現時点でのplayer1が成立したペアとして捨てたカード
  • dpair: 今回のプレイヤーのid(繰り返すがid1id2printに手渡された状態では既に更新済みになっている)。
の3つが完全に出力用情報である。
残りはplayersリストだが、これは先にも書いた通り、人間のプレイヤーの手札表示と、その他のプレイヤーが何枚カードを現時点で保持してるか、を表示する為に必要な情報となる。
それさえ押さえておけば各文字列整形関数に引数として適切な情報を与え、string-appendで結合してdisplayすれば良いだけ、だ。
ただ、この関数はもう一つハックがある。それは返り値として「受け取ったworld構造体を変化なしでそのまま返してる」トコである。
基本的にScheme及びRacketの組み込みでは出力関数は返り値を持たない手続きである。
一方、自作で出力関数を設計する際、敢えて副作用を利用した後、関数として何らかの値を返すようにしても構わない、のである。何故なら「その方が便利な場合がある」からだ。
と言うのも、現在、ゲームのREPLを作ってるわけだが、world構造体は大枠のループ内でread部 -> eval部(world-go関数) -> print部でグルグルと手渡されていき、なおかつeval部でバンバン加工されていく。
つまり、こういうルーピングを想定すると、world構造体の「存在」がどっかで途切れる、のはあまりアタマの良いやり方ではないわけだ。出来ればprintworld構造体を返し、それがスムースに次のルーピングプロセスに手渡された方が良い。
と言うか、そういう風にprintを設計した方が、このゲームにおけるREPLの書き様が俄然シンプルになるのである。

続く。

※1: もちろん、実際これは前方参照ではない。単なる「逐次処理」である。
これはあくまで「リストをプログラムの基礎とした」Lisp観点ではそう見える、ってだけの話である。

※2: 何度か指摘したが、長いLispの歴史の中では、letの登場は割に新しい方である。
旧いLispの代表格、と言えばEmacs Lispがあるが、Emacs Lispでさえ現在ではletが導入されている。
しかし、知ってる範疇で言うとEmacs Lispでは、少なくともANSI Common LispやScheme程letは多用されていない。じゃあ、変数への代入には何が使われてるんだ、と言うとEmacs Lispのプログラムで圧倒的に利用されているのは古き良きsetqなのである。
つまり、ここではPythonの代入文とRacketのletを同列として論じたが、厳密に言うと単にPythonはLispのletにあたる機能を持ってない、のである。Pythonの代入文に価するのは古き良きsetq、なのだ。しかもLispでもsetqを用いれば実はネストのレベルが深くなる、と言う事態は生じない。
言い換えると、Lispに於いてのletはまさしく「関数型プログラミングの為の代入代わりの新機能」と言って良い(つまりそれが「束縛」である)。これが無かった時はsetqで変数を破壊的に変更しまくるしか手が無かったのだから(とは言ってもフツーのプログラミング言語では良くやってる事である)。
なお、Emacs Lispでの典型的な書き方だとworld-goは次のように記述されるだろう。

(defun world-go(exp w)
 (setq clist (plist-get w 'clist))
 (setq id1 (plist-get w 'id1))
 (setq id2 (plist-get w 'id2))
 (setq players (plist-get w 'players))
 (setq lst0 (drawCard player1 player2 exp))
 (setq lst1 (discardPair (cadr lst0)))
 (setq players (insert id1 (cadr lst1) (insert id2 (caddr lst0) players)))
 (setq new-id1 (next-id clist (+ id1 1) players))
 (setq new-id2 (next-id clist (+ new-id1 1) players))
 (plist-put w 'card (car lst0))
 (plist-put w 'discarded (car lst1))
 (plist-put w 'dpair (cons id1 id2))
 (plist-put w 'id1 new-id1)
 (plist-put w 'id2 new-id2)
 (plist-put w 'players players))

一応、Pythonに負けず劣らずのフラットなコードが手に入る。
なお、原則的には

  • Emacs Lispには構造体がない
  • Emacs Lispには多値がない
  • Emacs Lispには副作用目的のマッピング関数がない
ので、まずは環境を示すシンボルwは属性リストだ、と言う事にしてる(ANSI Common Lispには存在するが、Schemeにはない)。考え方としては連想リストの変形だ、と捉えて良い。シンボルをキーとして中身を引きずり出せるのは全く同じである。
また、多値関数が存在しないので、関数drawCardと関数discardPairが返す値はデータを詰め込んだリストである、と言う前提にしている。
多分Elispではこんなカンジになるのではないか。

※3: C言語での悪名高いポインタと全く同じモノである。

※4: ヘンなリストだが、標準的なSchemeではset-car!及びset-cdr!を使って作成する事が可能だ。ところがRacketは基本的には書き換え可能(ミュータブル)なリスト構造を持っていないのでset-car!及びset-cdr!が存在しない。
よって、SRFI-1のライブラリとして提供されているcircular-listを用いないと循環リストを原則的には扱えない、のである。

※5: この辺の話はLand of Lispの102ページが詳しい。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事