ラベル PLT の投稿を表示しています。 すべての投稿を表示
ラベル PLT の投稿を表示しています。 すべての投稿を表示

2014年1月5日日曜日

Return Of イスカンダルのトーフ屋ゲーム

シリーズ(いつシリーズ化したんだ・笑)「イスカンダルのトーフ屋ゲーム」第3弾です。

まあ、今回は余談的に始めました。OOPスタイルでのSchemeでの「イスカンダルのトーフ屋ゲーム」の作成にかなり苦労したんで、フツーに関数型で書けばどうなるのか、自分で確かめてみたかったんですね。

作成自体は1日あれば終わったんですが、やっぱり慣れもあるし、ロジック的には同じの3回目だから、って事もあるんでしょうが、それでも「関数型で書く」ってのはやっぱ早いです。すぐ出来る。

加えるとテストがラクなんですよね。オブジェクトに対してメソッド呼び出して・・・ってのは、ぶっちゃけ、インタプリタでも書式がメンド臭いんで、結局「テストコード」を本体側に対して記述しないと色々とウザったい。
ある意味、テスト駆動開発ってOOP前提だよな、って気がしました。

なお、今回は原作者のページに置いてあるイスカンダルのトーフ屋ゲーム(ViViScript 版)~プロ仕様~の計算ロジックを参考にしてます。これ(と同じページに置いてあるオリジナル版)が原作者氏の書いたソースだ、って気づかなかったんですよね(笑)。実行形式が端末起動して遊べるようになってたから、まさか今公開してるのがエディタのマクロ言語で記述されてる、たぁ思いもよらなかったです(笑)。てっきりCかなんかで記述してると思い込んでました(爆)。

ってなわけで、初めましょうか。

ゲームに必要な大域変数の定義

まずはゲームに必要な大域変数を定義します。
関数型プログラミングではあまり大域変数使わない前提なのは確かなんですが、計算の過程に於いて、何らかのデータの「参照対象」が必要な場合があります。
重要なのは「参照の為に」定義するんであって、そのデータは絶対プログラム内では書き換えません。
まあ、そいつを引数で与えてやってもイイんですが、後に見ますが、スタイル的にはフツーのプログラミングに於ける「破壊的変更」を対象を新しく生成する事で避けてるんで、どうしても本体の引数が増えちゃうんですね。
ここで「大域変数」として定義した内容は、ゲーム全体に於いて使いまわされるけど、固定した値で変更する必要が生じないモノに限ってます。具体的には、


  1. トーフのプロパティ(値段、製造コスト等)
  2. Game-Overに達する目標金額
の二つです。今までの「イスカンダルのトーフ屋ゲーム」に於けるプログラミングを見てた人には、この二つはゲームを通して不変である、って事に気づくでしょう。
(ちなみに、ゲームに於いて表示される「文字列」も不変な故に大域変数対象になりますが、それは後にPrint部を作る際に定義しましょう。結局トータルでは3つの大域変数が必要となります。)
また、プログラム本体にこの手の「決まった数値」を埋め込むのは避けるべきだ、ってのも良く言われる事です。これらを関数の外側に出しておけば、ソースに手を入れて変更するのもラクですしね。
これら二つをトーフに於いては連想リスト、目標金額は単なる変数として定義します。



これで準備は万端です。ではまずはR->E->Pの順序に則って、まずはRead部から仕上げて行きましょうか。

Read (Parser 関数)

大まかなロジックは前回と全く同じなんですが、クラスに詰め込んだローカル関数を基本的には全部外に出してフツーの関数として再定義する事で、ソースは随分スッキリと読みやすくなります。
Common Lispに比べると、まあ、名前空間の衝突とか避ける為に、割にSchemeでは何でもかんでもローカル関数にしたがる傾向がありますが(実際、外人のCLerに「キミのコードはScheme臭い」って言われた事がある)、確かにローカル関数は場合によってはソースの可読性落としますね。
(ちなみに、基本的にはCLerはSchemer程ローカル関数は使わずに何でも大域的に定義したがるらしい。と言うのも「パッケージ機構」ってのがあって、名前空間の問題を仕様上回避するような仕掛けが施されている。反面そう言う大掛かりなシステムが"仕様上定義されてない"Schemeでは比較的神経質に関数を扱わざるを得ない。)
まあ、ケースバイケースなんですが、前回みたいに「ローカル関数の中にローカル関数定義」とかは多分やり過ぎなんで(笑)、そこまでするなら大域的に定義した方がスッキリするのは確かです。

なお、大まかな仕様は次の通りです。


  1. Parser関数(以降Readとする)は6つの(!)引数を受け取り、その中のphase変数の値を見て挙動を変更する。
  2. 変更される挙動とは...もういいやね、これ(笑)。
  3. 基本的には、引数をそのまま6つの多値として返すが、入力値があった場合にはそれをpnに束縛する。


ではソースです。


だいぶスッキリとして読みやすくなったのではないでしょうか。ローカル関数(letrec)も相変わらず使ってますが、やっぱ一つの関数内にネストが一個程度でやめておきたいもんですね。ローカル関数内ローカル関数は見た目には厳しすぎる。

ところで、実はここで前回までのプログラミングと大きく違う点があります。それは3つあって、


  1. 環境を作る仕組みがない。
  2. 環境は関数に引き渡される引数として表現される。
  3. 従って、ReadにもPrintにも(言わせれば)環境が引数として渡される。
前にPython版から作り出した時、「環境はEvalのモンだしなぁ」ってこだわってたんですが、実はここに来てそんなこだわりは必要がない、って事に気づきました。以前はそこにこだわっていた為、クラス変数と言うポスト大域変数の書き換え、みたいな破壊的操作が必要になってたんですね。
しかしながら、ここに来て、環境を手渡しつつRead、Eval、Printの間でグルグル回す方が良いのではないか、と気づいた。つまり、どの環境の変数に対してどの関数が手を入れるのか明確にする方が大事な気がしてきたんです(言わばバケツリレー方式ですね・笑)。
基本的には環境に対して計算結果を返すのはEvalの役目です。それは変わりません。しかしRead部はどっちにしても入力値を手渡す、って限定に於いては環境に手を入れても良いのではないか。つまり、ここでは「入力情報」がある意味「環境」になっちゃった、って事ですね。
上にあげた6つの引数のうち、 pn ってのが入力値を保持する為の引数です。つまり、何らかの入力があった際には pn は Readが扱う、と言うルールが明確になったのです。

Eval (Interp関数)

前までだとやれこのデータをクラスで定義して・・・だったんですが、今回は全く必要がありません(笑)。サクサクと進みます(笑)。
Eval部も基本的には、前回の「ローカル関数だらけ」を分離したスタイルになっています。お陰で、Eval(Interp関数)が一体何を行ってるのか、明確になる。

  1. Interp関数(Eval)は6つの引数を受け取る。
  2. シンボルで表された変数phaseを見て適する関数を呼び出し、次のphaseを束縛する。
  3. 変数 player はプレイヤーの持ち金である。加算減算が( calc 関数によって)返された場合は変数 player にその返り値を束縛する。
  4. 変数 comp は対戦相手の持ち金である。加算減算が( calc 関数によって)返された場合は変数 comp にその返り値を束縛する。
  5. 変数 wr は天気予報の値である。関数 weather-report の返り値を束縛する。
  6. 変数 pn はReadが読み込んだ入力値である。計算に必要な場合、この値を見て評価するが、pn に対しては特に何も行わない。
  7. 変数 cn は評価に於いて何らかのエクストラの情報が生じた場合(これは要するにPrintが要する情報になるが)この変数に評価結果を束縛する。
  8. 以上の6つを多値による返り値として返す。
さて、こう見てみると、一応Read->Eval->Printループなんですが、内部的にはEvalがスタート地点で、Eval->Print->Readと言うカンジで回っていくのが分かるんじゃないでしょうか。あらゆるデータに関する「評価」は文字通りEval(interp関数)が行っているんです。

ではソースの方を。



やっぱ分かりやすいと思います。Evalが必要とする個々の関数も全部小さいですし、ローカル関数で纏められすぎてるコードを読むよりゃやっぱラクですよねぇ。
一つあるトリックとしては、関数 actual-weather が多値を返してます。一つ目は一種フラグのシンボルで、このシンボルを使ってPrint関数が文字列情報をまとめた連想リストから適する表示形式を探してきます。つまり、変数 cn に束縛させるシンボルを返すと言うのが目的。二つ目はトーフデータが持つ「天気による最大売上個数」を連想リストから検索してて返してて、この結果はプレイヤーと対戦相手の「売上計算」に利用されてその後破棄されます。
このくらいですし、大したトリックでもないですね(笑)。まあ、多値利用しただけだった、ってばだけなんですが、意外と便利です(笑)。

Print 関数

さて、最後に Print 関数です。今回ははえぇなぁ(笑)。
最初に書きましたように、まずは表示に使うメッセージデータを連想リストとして纏めて大域変数としてまずは定義します。
実はこの辺、前回も指摘したんですが、SRFI-48で定義されてるFormatを使用すれば、書式の指定子込みでデータを作れるんで、もっと簡単になるでしょう。
今回もPrintが利用してる関数は、わざわざ整形演算を行う為に書いたモノが殆どで、format 前提なら殆ど本体だけで間に合う、って言っても過言じゃないと思います。


  1. Print 関数は6つの引数を取る関数である。
  2. ただし、印字を出力後、この6つの引数を多値としてそのまま返し、引数に対する操作は一切行わない(これがまさしく印字が副作用的性質である事の証明・笑)
  3. 基本的には、phase 引数を見て適した文字列を連想リストとして設計された message データから検索してくる。
  4. 出力に必要であれば、cn 変数を用いる。
これだけ、です。コードがそれにすれば分量が若干多いように見えますが、もう一度言いますが、SRFI-48でのformatとか利用すれば殆ど Print 関数本体だけで仕事をやっつけちゃえるのです。
(恐らく連想リストの設計さえも、phase と一対一対応で、理想的には phase を利用して検索して表示するだけ、までに構造的には絞り込めると思います。Lispのシンボルの有用性の証明になる気がします。)

ではソースです。



とこれで、Read、Eval、Printの全てのパーツが「関数型プログラミングを使って」組みあがりました。(もっともHaskellな人達は簡単に出力を扱える事に違和感があるでしょうが・笑)
あとはREPLを組むだけ、ですね。

Read-Eval-Print-Loop

REPLは次のように末尾再帰な関数として書いて、実行時に適切な引数をREPL関数に渡す事でゲームが動きます。


まあ、多値を6つも返す関数が3つもあるんで物凄い事になってるんですが(笑)、基本的には簡単ですね。関数REPLは末尾再帰で呼び出された際に、最終的にPrintが渡した値を引数として持って、延々と無限ループへと突入するわけです(笑)。Evalが(exit)命令を出すまでこの動きは止まりません(笑)。

なお、phase の初期値として #f を渡してるのは、初期状態で、 #f はParser 関数( Read ) 本体のcase 構文内で「else」として認識され、そのまま「何もせずに」 interp 関数に全引数を渡すからです。
続いて、interp 関数は phase = #f を見て、やはり本体の case 構文内で else として処理され、ここで初めて、phase にシンボル 'introduction が返り値の一つとして束縛されるわけです。
つまり、初期状態で phase に与える値はゲーム中で使われてないブツなら何でも良くって(あるいは意図的に「途中から」始める事も可能) 結果、伝統的で単純な #f を与えてみました。
ちなみにそうなると、最初の Read -> Print 間は全く何もしてないに等しくて、引数与えるのは「REPLをビックリさせて起動させる」に等しいです(笑)。また、先ほどにも書きましたが、内部的にはE->P->Rの順に処理が進んでいく、ってのは全くそのままの意味です。

非破壊的なゲーム構造

まあ、ちょっとしたシャレで始めたこのヴァージョンだったんですが、面白い事に気づきました。
実は、REPLで末尾再帰が呼び出される際に、与えられる引数はPrintの返り値です。
んで、実はこのプログラムの中では「一回も」普通のプログラミングで言う「代入」は行われていないんですね。つまり「直接変数を書き換える」と言う事は一切行っていない。
特にEvalの動作なんですが、


  1. 引数を参照する
  2. 基本的には「操作」は新しい値を「作る」
  3. 他のケースでは「与えられた引数を参照して」(要するに)コピーしてる
  4. 結果、返り値は元のデータではなく新しいデータである。
この「コピーして」「新しいデータを作る」ってのが言わば関数型プログラミングのキモなんですよね。じゃあ参照に使われた古い引数はどうなるのか、っつーと、参照されなくなった時点でGC(ガベージコレクタ)がやってきて回収・廃棄処分です(笑)。
これは実はReadにせよPrintにせよ、「コピーして新しいデータを作ってる」ってのは同じです。「書き換え」ではないんです。
まあ、こんなこたぁ関数型言語に詳しい人には釈迦に説法なんですが、ちと待てよ、と。こんな事が出来るなら恐らく「非破壊的に」データ生成し続けてゲームを作れるモデル、例えば理論的にはRPGなんかも作れるんじゃないか、って思ったんですね。で、多分それは可能でしょう。
以前、関数型プログラミングは机上の空論なんじゃないか、って実は失望してたんですが、ひょいとしたキッカケで実用上OKなんじゃないか、って思えるようになったのは大収穫でした。
つまり、上のREPLのような関数を設計して、参照する環境として引数(例えばRPGで言うとプレイヤー情報とか)を与えて、内部的にはコピーして元データには一切手をかけない。そうすれば「非破壊的なプログラム構造を持った」関数型による関数型の為の関数型のゲームが作成可能だ、って光明が見えたんですよね。
引き続きこのテーマを研究してみたいと思います。言い換えると、例えばOOPモデルでさえ(プログラミング言語の末尾再帰の最適化が保証されていれば)インスタンス変数を「書き換える」のではなくって「新しいインスタンスを」常に生成するモデルも可能だろう、って事ですから。

では今回の全ソースコードです。



イスカンダルのトーフ屋ゲーム Strikes Back

年末年始かけてイスカンダルのトーフ屋ゲームの実装ばっかやってて一体何をやってるんでしょうか(笑)。

さて、今回は前回と同じゲームなんですが、Schemeで実装してみました。

SICPのデータ抽象型のプログラミング示唆にもある通り、クロージャを使いまくってプログラミングすればどうなるのか、一回やってみたかったんですね。それがOOPの理解も深めるだろう、とか思って・・・。

結果は・・・サイテーです(笑)。
まあ、言語形式もあるんですが、少なくともSchemeでOOP系のプログラミングやるのは頭痛の種以外の何者でもない、ってことが分かりました(爆)。
取り合えず関数型プログラミングとOOPとどっちがマシか、って話は脇に置いておきますが、少なくともLispのようなカッコが多い、と言うかプログラミングコードがかなり深いネストを生じる言語ですと、OOPのコード書くのはキツい、かつ、読むのもキツい、って思いますね(笑)。OOPはPython等のフツーの言語、言い換えるとネストがそんなにキツくならない言語に限って「スタイル的に有効だ」ってのが今回の実験での結論です。
Schemeのスタイル(あるいはLisp一般での関数型プログラミングのスタイル)、ってのは、基本的には


  1. 暗黙のbegin(あるいはprogn)をアテにしない。
  2. 従って一つの関数には一つの処理しかさせない小さな関数をたくさん書く
  3. 小さな関数同士を組み合わせて大きな処理をさせる
って事で、極めてシンプルにプログラムを組み上げる事です。これはUNIX的プログラミングの方策としては王道ですし、かつ、これだと大してネストが深くならないので、カッコだらけでもそんなに読むのはツラくありません。
反面、OOPと言うかデータ抽象型ですと、一つのオブジェクトに如何にしてたくさんのデータと処理をまとめるか、って事になって、一つのdefineされたモノが異様に膨らんでしまいます。もうこうなると理想的なSchemeコードとは言えないでしょう。
ま、てなワケで実験としては有効でしょうが、真似しないようにしましょう(笑)。自分で書いてて「こりゃヒドいコードだ」とかアタマ痛くなってましたから(笑)。

さて、今回の方針です。

  1. 新しいロジックは採用せず、基本的に前回作ったPython版のコーディングを参考にする。
  2. Schemeデファクトスタンダードの仕様、R5RSになるたけ準じて、Racket実装依存の機能はなるべく使わない。使った場合は明示して、他のScheme処理系で動かす場合、最低限の変更でOKなモノを目指す。
  3. 足りない機能があったらSRFIから持ってくる。
の3つです。1番目は当然で、「実験」でスクラッチからロジック考えてればアタマ痛くなってきます(笑)。
2番目もこれがなかなかSchemeだと難しいんですが敢えての挑戦です。どうも個人的な意見では、Webで見つけた面白そうなSchemeコードでも、作者の処理系選択によって、あっちじゃ動くけどこっちじゃ動かん、とか結構腹が立つんですよね(笑)。特に僕が使ってるRacketの場合、Racket実装依存で書くには便利な機能がテンコ盛りなんですが、他のScheme処理系じゃ動きません、っつーのはあまりにもアレなカンジがします。
3番目も初めての挑戦ですかね。SRFIは「共通ライブラリ」とか言われたりしますが、本当は個人的にはあんま好きじゃなくって、と言うのも仕様定義だけで実装は各実装に任せる、と。要するに外部的にライブラリがあって、どのScheme処理系でもインポート出来る、ってポータブルな意味でのライブラリじゃないんで嫌なんです(笑)。まあでも、今回は、少なくともGaucheとGuileの2つで最低限の変更で動かせる為にSRFIサポートをチェックしてそのようにしました(Scheme48も考えたんですが、こいつはそもそも使えるSRFIが少ない)。

では始めます。

データ抽象化: 擬似OOP

ところで。「実装依存の機能をなるたけ使わない」「なるべくR5RS内で事を済ませる」となると・・・データ抽象型プログラミングを実現するにはそれなりの機構と自分で実装せなあきまへん(苦笑)。
もちろん、SICPよろしく、全部細やかにクロージャ使って実装していくのも可能なんですが、それじゃああんまりにもメンド臭すぎます。
そこで。
Paradigms of Artificial Intelligence Programmingを参考にして、まずは擬似OOPとも言えるシステムをR5RSマクロで組んでいくのが最初に行う事です・・・ってこれがまた大変だったんですけどね(苦笑)。
ではまずそのコードを。

一応、Schemeと言うかLisp一族に明るくない人の為に解説しておきますが、簡単に言うと、Lisp/Schemeで言う「マクロ」とは、言語に新しい構文規則を付け足す機能です。つまり、Scheme/Lispでは、極端に言うと、C言語で言うdo~whileなんかの「構文」を自分で新しく作れちゃうんですね。
ここではそう言うカタチで新しく「擬似OOP」な機構を作り出してるんです。
んで基本的なコードのアイディアってのはSICP第三章で紹介されてるモノとも同じですね。

ところで、残念ながらSchemeのR5RSで策定されてるマクロはCommon Lispのマクロに比べると遥かに貧弱で、PAIPで紹介されてる実装のスペックには届きませんでした。その一つとして継承機能が組み込めなかった。
もっともPAIPで紹介されてるOOPマクロが継承可能なのは、中で使われてるmessage変数がgensymされてないんで、そこに子クラスの条件ツッコむとcase内の選択肢が増える、と言う極めて危ない実装となっています(笑)。それで考えるとSchemeのマクロが安全なんですけどね。
それから、PAIPの実装だと、クラス定義した時点でメソッドが自動で定義されるんですが、これがR5RS Scheme マクロだと不可能でした。そこでしょーがないんで、外部的にdefine-methodと言うマクロを作って、それでメソッド名をわざわざ打ち込む形式にせざるを得ませんでした。CLOSと普通のオブジェクト指向の間の子のようなスタイルになってしまいましたね。
あと、Peter Norvigによる実装ですと、クラス変数がdefunを包んでるように定義されていて(クロージャで囲まれてる)、このスタイルだとやはりそのままSchemeでは実現出来ません(Schemeだとこの場合ローカル関数扱いになってクラスが外から全く見えなくなってしまう)。そこでPythonのようなクラス変数へのアクセスも考慮して、思い切ってクラス変数は大域変数としてクラス外で連想リストとして定義して、そこへのアクセッサをget-class-var、class-var-set!として定義しています。ちと不格好なんですが、前回見たとおり、使いドコロによっては便利ですが同時に危険なクラス変数へのアクセスは明示的に不便な薫りを漂わせておくべきだ、ってのは結果悪くないんじゃないか、とは思います。
ちなみに、連想リストを使ってる+Racketの厳密なクオート適用のせいで、原作ではフツーの関数だったけど、ここではマクロにせなあかんかったのが何個かあります。


テスト実行は以下のとおりです。




と言うわけで、ちと不格好なOOPモドキではありますが、これらを駆使してScheme版「イスカンダルのトーフ屋ゲーム」を書いていこうと思います。

Parser クラス (Read)

さて、前回は結構勢いだけで作っていって、「どう言う順番で組み立てていくべきか」とかあんま考えてなかったんですよね(笑)。結構行き当たりばったりでした(笑)。
まあ、今回は二回目ですし、また参考に出来るPythonのコードもあるし、ってな事もあるんですが、しかしそれでもどうやらやはり

REPL構造はRead->Eval->Printの順序で組み立てていくべきだ

って事がちと分かりましたね。
考えてみれば当たり前なんですが、情報の流れを追った順序で組み上げていくのが原則で、逆方向に進むとか、あるいはEvalから組み上げてみる、ってのはどうやら具合が宜しくない模様です。
んで、今回もReadに関する指針は次の通りです。


  1. Perserクラスはクラス変数としてphaseを持つ。また、メソッドはinputメソッド(ここではReadと呼ぶ)のみを持つ(正確には「のみ」しか持てなかった・苦笑)。
  2. Readの挙動は「何も入力が無くてもEvalに情報を渡す」「整数の入力しか受け付けないでその情報を含めてEvalに渡す」「Yes/Noに類する入力だけ受け取ってその情報を含めてEvalに渡す」の3種類とする。
  3. 挙動の変更はゲームのステージを表すクラス変数、phaseを参照して行う。Phaseにはゲームステージがシンボルとして束縛されている。
  4. Read は2値を返す。一つ目はphaseの値をシンボルとして、二つ目は入力値、あるいは入力されない場合は #f (Schemeの偽)返す。
以上の3つです。基本的には1つ目も2つ目も前回のPython版と同じです。
違う点と言うと、まずは、phaseに束縛されてるシンボルと言うLisp族特有のデータ型なんですが、Pythonでは文字列を返した代わりに使っています。
もちろんゲームステージに対して番号ふる処理にしてもいいんですが、やっぱり組んでる人間としては「どのステージでどう言う処理を」って明示された方が分かりやすいんですよね。しかしながら、一般に文字列情報は比較等の演算で時間がかかると言う欠点があるんです。
反面、Lisp族のデータ型シンボルとは、評価過程に於いて唯一無二と言う事が保証されていて、ポインタ比較だけで何の情報が渡されたのか、とすぐ分かるんですね。理論的には文字列を受け渡ししているPython版なんかより遥かに高速に比較処理してくれます。これが大きいです。
しかもプログラム組んでる人間にもどう言う情報で・・・と言うのが一発で分かりますし、Lisp内に於いては数値比較よりも高速だったりします。至れり尽くせり、ああシンボル万歳、たぁLisp好きの合言葉ですね(笑)。
あとは、3番。前回のPython版は二つの情報をまとめた「タプル」と言うデータ型でEvalとやり取りさせてたんですが、このScheme版では本当に二つ値を返しています。これも一部のLisp(特にCommon LispとScheme)では有名な不思議な機能、多値関数と言う機能です。手続きvaluesで包んでやるとあら不思議、いくつもの値を一気に返せるんですってよ、奥様!
まあ、実際はここで使う必然性は実はあんま無かったんですが(笑)、何となくやってみたかっただけです(笑)。実際ホントの事言うと、リストとか便利過ぎて、通常あんま多値の出番がないんですよね(笑)。

まあ、てなワケでコードを見てみますか。ぶっ飛びますよ(笑)。


さあて、どうでしょうかこれは(笑)。
まずは予告してた通り、Read自体がデータ抽象として設計されてる為、一つのSchemeの手続きとしては明らかに長すぎる印象があるんじゃないでしょうか。つまり、印象的には何でもかんでも詰め込み過ぎてる関数に見える、って事ですね。
加えると、PythonのOOPの場合はselfがあるんで、メソッドをクラス内でバラバラに定義してもお互いに参照可能でした。一方、まあ、今回実装したクラス定義があまりに貧弱だ、って事もあるんですが(笑)、クラス内で定義されたメソッドにクラス内で定義したメソッドは基本的にアクセス出来ません(笑)。自分でやってて「何じゃこりゃ?」とか思ったわけですが(笑)。
詳しく説明すると、define-methodで定義したメソッドはインスタンスを通じてしかメソッド本体にアクセス出来ないんです。

メソッドの書式:
(メソッド インスタンス 引数 ...)

ところが当然、今のように「クラスで定義してる最中にメソッドへのアクセスが出来ない」、つまり内部的に参照しあえない場合は・・・そうですね、メソッド内部でローカル関数として必要な機能を定義してやるしかない、って事になります(だから上のコードはletrecだらけなんです)。
いや、今までPythonのself嫌いで文句ばっか言ってたんですが(笑)、今回初めてPythonのself機構が好きになりましたね(笑)。自分で実際OOP作ってみて分かるその偉大さ(笑)。ああ、そう言う理由でああなんだ、とか実装してみて初めて分かる事ってあるんだなぁ、ってカンジです。
もっとも、良く言われるんですが、PythonでのOOP機構は全部パブリックなんで危険だ、って話もあって、それで言うとプログラム全体で必要な機構をメソッドで外部に出して、他の内部用演算はローカル関数として外部から隠す、と言う上のような書き方の方が安全、って言えば安全かもしれません。ただし見た目の酷さを除けば、ですが(笑)。

取り合えずテストしてみましょうか。実はこれがちょっとしたトラブルがあるんですが、まあ、大まかには予定通り動きます(笑)。

とまあ、最初の方はテストが通るんですが、途中でRacketがおかしな挙動になるんですよね(苦笑)。成功する場合もあれば失敗する場合もある。なんじゃそりゃ(笑)。
実はRacketの場合、(Lisp族にしては)ゴージャスなIDE付き、って事もあって、なんか特にReadで呼び出される入力機構が立派過ぎるんです(笑)。多分その関係で負荷が高く、平たく言えばこの手の入力テストやると(current-input-port)なり(current-output-port)のflushが上手く行かないんじゃないでしょうか。PLT Schemeの頃はこんな事無かったんだけどな(苦笑)。

まあ、取り合えずテストは通った事にして先に進みますか(ヲイ

Player Class / Computer Class / Tofu Class / Weather Class

先ほども書いた通り、REPLは大まかにはR->E->Pの順序で組み上げて行った方が良い、と言いました。
実は、結構な確率で良くある「LispでLispを作る」例ではこの原則が守られてないんですよね。と言うのも、大体のケースでは「ReadとPrintはLispのそれらを流用する」ってのが前提で、SICPやPAIPなんかの本もその全精力を「Evalの構築」に注ぎます。だから実際にREPLモデルを組む際の順序、と言うのには無頓着なんです。例えばPAIPの著者、Peter Norvigが書いたこのページなんかもそう言う意味ではグチャグチャで、やはり読み込むデータをどう言う風にパーズして渡すのか、決定してからEvalを書いた方が本当は実際的なんじゃないか、って思います。もっとも、本当はそうやってても、テキストを書いた時点で順番入れ替えてるのかもしれませんが。
んで、Eval作成時時点でEvalで使うデータから作っていくわけです。基本的には評価機構がEvalである以上、扱うデータのその殆どは(Printで扱う文字列を除き)Eval絡みになるんじゃないでしょうか。
そんなわけで、まずは(Python版と違って継承が無いので)プレイヤーを表すPlayer Classと対戦相手を表すComputer Classとをそれぞれ、そしてトーフを表すTofu Class、天候を司るWeather Classを作成します。




この辺はイイですよね。ロジック的には前回のPython版とほぼ同じですし、シンプルです。フツーに構造体が使える言語でも大体近いカタチで落ち着くんじゃないでしょうか。

Environment Class

で、上記で定義したクラス群を内部に保持する為のEnvironment Classを定義します。これもシンプルに定義出来ますね。




これもロジック的には前回とほぼ同じなんでまぁ良いでしょう。ただし、各インスタンスにアクセスするアクセッサもここで定義しておきます。
テストコードの実行結果は以下の通りです。



Lispらしくテストコードでマッピングとかしてみたんですが(笑)。しかし、メソッドの性質によって、どっちがどっちにマッピングしてんだかイマイチ分かりづらいですね(笑)。構文的には逆になってるように見えます(笑)。
ちなみに、例えば(get-player e)ですとこの場合、平たく言うとPlayer Classのインスタンスである、実際のplayerにアクセスしています。要するにplayer変数にアクセスしてるんですね。そしてそれが持ってるインスタンス変数を全部表示するようにして、問題無く各値にアクセス出来る事が分かります。
と言うわけで、いよいよEvalの実装です。

Game-Master Class (Eval)

Game-Master Class(Eval)の大まかな仕様は次の通りです。


  1. インスタンス変数として環境(env: Environment Classのインスタンス)を持つ(もう一つは、YES/NO入力に対しての挙動を変える為のStrange Flagってのをフラグとして持ってますが、これはロジック的には本質的なモノじゃないです)
  2. 唯一、interpメソッドを持っている。これが普通の意味ではEval本体に当たる。
  3. interpは二つの引数を持つ。一つはphase情報、もう一つはReadから渡された入力値、あるいは#fである。
  4. interpはphase情報に従って評価を下し、また次のphase(ゲームステージ)をParserクラスのクラス変数としてセットする。
  5. interpは出力指示情報と計算結果(あれば、あるいは#f)の二つを多値として返す。

とまあ、書く分にはやっぱ簡単で(笑)、特にphaseとして渡されてきたシンボルをcaseで事業仕分け、もとい(古い・笑)適した関数呼び出せばイイんですよね。ロジックは簡単なんですが・・・。
やっぱりPython的にselfで自分のメソッドを呼び出せないんで(笑)、結果としては鬼のようなローカル関数使いまくり、って形式になっちゃってます。

ではソースです。



何でしょうね、これは(笑)。ローカル関数内にローカル関数があったりして、たまったモンじゃないです(苦笑)。だから言ったでしょ、フラット(インデントレベルが深くなり過ぎない構文を持つ)に書ける言語にはOOPは合いますが、そもそもこう言う「ネスト大好き!」な言語にはOOPは向かないんですって(笑)。書いた本人でも「何書いてたっけ」って迷子になりました。Python以上ですね(苦笑)。

ではテストコードの実行です。


コードが長い割には結果はシンプルだと言う・・・(笑)。
ま、いっか(笑)。んで、返り値の二つは次のPrint クラスへと手渡されます。

Message Class (Print)

さて、残るはPrint部分だけです。前回のPython版では、ロジック的には「整形表示」を司ってただけでいっちゃん簡単でした。
が・・・・・・。
R5RSでdisplayだけ頼りにして整形表示を自前で計算させる、って無謀な事をやっております(爆)。しまったな、SRFIからformat引っ張ってくれば良かった(とか思っても後の祭り)。
まあ、SRFIを読みこめば読み込む程実装依存度が結果高まってしまうんで(先にも書いた対応処理系自体の事情)しょーがないですか。まあ、formatさえあれば、ホントもっと短く済みます。ホントロジック的には大した事ないんです。

大まかな仕様は以下の通りです。


  1. Message クラスは data と言うインスタンス変数を持ち、それはゲーム中で使う文字列からなるデータに束縛される。より正確には、文字列データをシンボルによりラベル付けされた連想リストがインスタンス変数 data となる。
  2. Message クラスは唯一 Print メソッドを持つ。
  3. Print メソッドは2つの引数をEvalから受け取るものとする。一つ目はどの表示をするか、と言う情報をシンボルで受け取り、二つ目は Eval が出した何らかの評価結果、あるいは #f である。
  4. 評価結果を利用した表示を行う(あるいは#fが入ってきた時は使う必要がない)。

以上です。簡単ですね。
ではコードを。

ご覧の通りです。
デカくなってる理由は仕様にも書いた通り、ゲームで扱う文字列情報を連想リストとして持ってる為、また、R5RSに準じると、文字列整形用に指定子等が使えない為、ローカル関数の殆どは「整形用の演算」を行ってるのです。先に書いた用に、SRFI辺りからformatを持ってくるとかすれば殆どの処理は書かずに済むでしょう。
(例えば数値表示をケツで揃える為、頭に何個スペースを入れるか、なんて計算したりして、自分でやりながら笑ってました・笑)

ではテストコードです。


おお、やっぱ表示が出てくると一気にゲームっぽさが出てきますね。ここに到達するまでが大変で、それまではテストコードなんか打っても無味乾燥な値が返ってくるだけですしねぇ(苦笑)。
まあ、もちろん説明するべくもないでしょうが、このテストコードに引数として与えた値は、実際Evalが返すだろう事を想定した値となっています。
では、いよいよREPLを組み上げます。

REPL(Read-Eval-Print-Loop)

「組み上げる」とは言っても、ここまで来ると大した仕事は残ってないんですけどね。
以下にREPLのコードをあげておきます。



基本的には大したこたぁないです。
注意点は、今回はRead、Eval、Print、の3つとも多値(2値)を返すメソッドとして実装されてるので、SRFI-11のlet-valuesと言う機能を用いてそれら多値を束縛して次のメソッドに渡すようにしています。
まあ、一番最初の方で書きましたが、Schemeプログラミングに於いてあんま多値使う局面ってそんなに無いんで、使わないで、例えばリストなんかで結果を返しても良いんですが、一方、consは結構計算コストがあるんで、そう言う意味では多値を使う事である程度の「速度向上」は見なせる模様です。

イスカンダルのトーフ屋ゲーム (Scheme 版) のソースコード

では、全部のソースコードを改めて貼っつけておきます。


こう見てみると各クラスは大きいわ、長いわ、でサイテーですね(笑)。まあ、コメントなんかもありますが、600行近くあります。
「Schemeらしく」関数型でプログラミングすればもっと縮まるんじゃねぇの、ってんで、実は既に関数プログラミング版作ってみたんですが、案の定 2/3 程度に圧縮されました。しかももっと綺麗(笑)。破壊的操作が一切ありません。
やっぱLisp系には少なくともOOPは向かねえんじゃねぇのかなぁ。良く、グラフィックスに関しては昔はLispが優れてた、とか言いますが、OOP前提での今の世の中ですと、昔日のアドヴァンテージはあんま持てないんじゃないでしょうか。OOPで書かれたライブラリとやり取り(特に多重継承)して、CLIをGUIに簡単に変更、ってなわけにゃ行きそうもないですから。
まあ、あくまで個人的にOOPをもっと理解したい、って為の実験ですし、OOP使うならPythonで書いた方がよりシンプルっぽいですね。

ちなみに、今回も実行形式作ろうってトライしてみたんですが、Racketのコンパイラが文字コード周りにバグがあるらしくって、日本語でコンパイルすると作成されたexecutableがすぐ落ちちゃうんですね。
そんなわけで、今回は実行形式は無し、です。

2012年12月7日金曜日

defineq

Interlispにはdefineqと言う特殊形式がありました。これはグローバル環境で一気に関数を複数定義出来る機能です。Common Lispで言うとlabelsの大域関数版、Schemeで言うとletrecのグローバル手続き版、って言ったところでしょうか。
Scheme(R5RS)でのdefineqの定義は以下の通り、です。
まあ、シンプルで大したマクロじゃないですね。取り立てて役に立ちそうには見えないですし。
何に使うか、みたいなのは次回で。
ちなみに、SchemeのR5RSマクロでの大域環境での何らかのセッティングは、複数のテンプレを一気に変換先として設定しても上手く反映出来ません。と言うかまさしく引っかかるのは複数のdefineなんですけどね。
唯一defineを、クロージャ関係無しに囲めるのはbeginだけ、なんで、それこそお約束通りに「逐次実行」のbeginで、こう言う場合は包んでしまいましょう。

2010年7月20日火曜日

Goldbach's conjecture

A list of prime numbers

Compare the two methods of calculating Euler's totient function

Calculate Euler's totient function phi(m) (improved)

Determine the prime factors of a given positive integer (2)

Determine the prime factors of a given positive integer

Calculate Euler's totient function phi(m)

Determine whether two positive integer numbers are coprime

Determine the greatest common divisor of two positive integer numbers

Determine whether a given integer number is prime

2010年7月19日月曜日

Sorting a list of lists according to length of sublists

Group the elements of a set into disjoint subsets

Generate the combinations of K distinct objects chosen from the N elements of a list

Generate a random permutation of the elements of a list

Lotto: Draw N different random numbers from the set 1..M

Extract a given number of randomly selected elements from a list

Create a list containing all integers within a given range