なぜ大改造が必要だったのか? - crest-cassia/CrowdWalk GitHub Wiki
CrowdWalk を、ほぼ作り直しに近い状況で改造した。 その理由を、改変事項ごとに記す。
大事な点は以下の通り。
- プログラムは、部分的に読んでも機能や構造がすぐにわかるようにしなければならない。
- 名はかならず体を表さなければならない。名前から想像できないものは決して作ってはならない。
- 同じ概念は同じ名前に、違う概念は違う名前にしなければならない。
- 整数・実数・文字列・論理値など組み込みの型を使う場合は、 それが表すデータが、その型が持つ性質を有するものでなければならない。
- オープンソースで公開するプログラムであるなら、上記を徹底しなければならない。
- うちわで使うだけのプログラムであるなら、そのプログラムを作成する理由が、プログラム作成以外になければならない。
- 具体的には、プログラム以外の研究成果(数学的理論の検証ぐらいしか思いつかないが)がないと、意味がない。
- プログラムの中身をいじらせずに他人に使ってもらうプログラムを提供するのであれば、
プログラムの完全性を(ある程度)保証しなければならない。
- 見通しの悪いプログラムでは、プログラムの完全性を示すのは絶望的である。つまり、価値を持たない。
- うちわで使うだけのプログラムであるなら、そのプログラムを作成する理由が、プログラム作成以外になければならない。
- 「人工知能」の研究をするならば、絶えず、手続きを抽象化することを考えなければならない。
- 手続きを、単にプログラムで表現するのは、「人工知能」ではない。
- 人工知能は、データの処理の仕方の優劣を探求する学問ではなく、処理の仕方を改変していく仕方を探求する学問。
- このためには、「処理の仕方の改変の仕方」というメタレベルの処理を論じれないと意味がない。
- 「メタレベル」とか「抽象化」とは、「処理の仕方」をデータ化して、そのデータを扱う処理をプログラムすること。
- データ化することができれば、処理の順番や条件付き実行はプログラムの外から制御できる。この制御こそが人工知能。
- 処理をデータ化するデメリットは、コンパイラによる最適化が出来ないところ。 逆に言えば、コンパイラによる最適化が効果を持つほど頻繁に実行されない部分であれば、データ化のデメリットはほどんどない。
- 手続きを、単にプログラムで表現するのは、「人工知能」ではない。
- 技術的負債は決して作ってはならない。
SimTime
と SimClock
の導入
- シミュレーション内時刻管理は、シミュレーション開発では一番根幹の部分。 シミュレーションの進行管理、シミュレーション内での計算、結果の出力や表示、すべてにかんけいする。 どういうシミュレーションであるかを形作る要であり、最新の注意と考察をもって設計しなければならない。
- もともとの実装は、その重要さを無視していい加減な設計をしたがために、見通しの全く効かない構成になっていた。
- もともとの実装では、時刻は実数値(
double
)として表されていた。 - しかも、その値の基準は、シミュレーション開始からの相対時刻の場合と、
設定ファイルに記された絶対時刻(
"HH:MM:SS"
の形式)を実数表現にした二通りが混在。 - 一方、プログラム中では、これを表す変数は
time
だけで、相対か絶対かの区別も何もなし。 - プログラム開始の時刻はシナリオの中にしか無いため、相対・絶対の変換や、文字列化はそこを参照できないとダメな構造。
- もともとの実装では、時刻は実数値(
- そもそも、double で時刻を表せるとした根拠が不明。
- CrowdWalk は、一定刻み幅による離散時間シミュレーションであるはずである。
であるなら、時刻表記は整数値であるべき。
実際に、
EvacuationSimulation
にはそれを表すtickCount
なるものがあった。 しかし、実際のシミュレーションの計算では、それにいちいち刻み幅であるtime_scale
というもの(この言葉も変だ)を 乗じて実数値のtime
を求めていた。 計算で気にしなければならない、実数と整数の精度の差や性質の違いなど無視している。
- CrowdWalk は、一定刻み幅による離散時間シミュレーションであるはずである。
であるなら、時刻表記は整数値であるべき。
実際に、
- これらの問題を解決するために、相対・絶対時刻の区別なく、
「時刻」や基準時刻、刻み幅情報というものを表すクラスを導入せざるえない。
それが
SimTime
。SimTime
は、相対時刻・絶対時刻の数値表現・文字列表現との相互変換機能を用意してある。 これにより、SimTime
のデータさえあれば、時刻をちゃんと記録できる。
- さらに、それがちゃんと離散時間の時を刻むのを確実にする時計の概念を表す
SimClock
を導入。SimClock
からは容易にタイムスタンプの形でSimTime
を取り出せる。 プログラムの大半ではこれを使って時刻に応じた処理を記述できるようになる。
名称の変更
dv
を accel
に
- もし、速度が
v
もしくはvelocity
であったなら、dv
を使う理由は存在した。 しかし、実際には速度はspeed
という変数名を用いている。 であるならば、dv
という変数名は全く意味を持たなくなる。
move_set()
、move_commit()
を advanceNextPlace()
、moveToNextPlace()
に
- そもそも、プログラム中に
move
という変数も、それに相当する概念を表すデータもない。 - 以前に導入した
Place
を使って、現在地を表すcurrentPlace
と移動先の位置を表すnextPlace
はすでに導入されていた。 - それに対応する形で、変更。これにより、メソッド名で何を計算するかが、ある程度推測できる。
navigate()
、sane_navigation_from_node()
、renavigate()
の変更
- これらのメソッド名は、全く意味不明である。
renavigate()
は、決して、navigate()
を再度行うことではない。navigate()
と、sane_navigation_from_node()
の関係が全く不明。- navigate という単語で何を表そうとしているか不明。
- これらの問題をかいけつするために、各々、
chooseNextLink()
、chooseNextLinkBody()
、prepareRoutePlan()
に変更。navigate()
がやっていたことは、ノード上に於いて、次に進むべきリンクを選択することである。
calcWayCostTo()
を calcCostFromNodeViaLink()
に
- way という概念は、プログラムのデータの中にはどこにも出てこない。なので、何を計算するのかまったく不明である。
- way は pathway の way のつもりらしいが、データ構造の何に対応するものはない。 MapLink というかもしれないが、MapLink なら link とすべきである。 新しい用語を導入するなら、way と link で何が違うか、明確にしなければならない。
- なので、シンプルに、何を計算しているかを表すメソッド名に変更。
Launcher、Simulator、Editor の整理。
- このあたり、何の考えもなく命名されていた。 なので、simulator の中に simulator がある、など、歪な構造があちこちに見られた。
- これらの概念は、どういう階層関係・包含関係にあるかが、名前から推察できなければならない。
- もし出来なければ、後からこのプログラムをいじる人間は、プログラムをすべて読まなければならない。 そんなことをさせてはいけない。
enum MapLink.Direction
の導入
- それまで、lane の方向は 1.0, -1.0 という実数値で表されていた。
- 何かを実数で表す場合、その「何か」は本質的に実数の性質を持たねばならない。
しかし、どう考えても、進行方向が実数で表される意味付けはない。
- 実数とは、以下の性質を持つものである。
- 1次元の連続の値。
- その濃度は実数濃度。
- 全順序関係がある。
- 2つのデータの間の「距離」あるいは「近さ・遠さ」が表される。
- 実数とは、以下の性質を持つものである。
- さらに、プログラムを書く場合には、そのデータが単体で現れた場合でも、
それの意味するところができるだけそれだけでわからなければならない。
- わからなければ、後でそのプログラムを読む人は、全プログラムを理解せねばならない。
- そのため、整数値やboolean を使うことも避けなければならない。
- 何かを実数で表す場合、その「何か」は本質的に実数の性質を持たねばならない。
しかし、どう考えても、進行方向が実数で表される意味付けはない。
- lane の方向という情報は、順方向か逆方向、予備として、そのどちらでもない、という3つを区別できれば良い。
- まずいちばんに必要なのは、「区別」できること。これは、「距離」がプラスであることではなく、
「違う」か「同じ」かの判定ができること。
- 類似性と同一性は、全く違う概念であり機能である、ということをちゃんと認識できないといけない。 類似性計算が必要なデータは、実数の方が親和性が高い。 同一性計算が必要なデータは、整数や論理値方が親和性が高い。
- まずいちばんに必要なのは、「区別」できること。これは、「距離」がプラスであることではなく、
「違う」か「同じ」かの判定ができること。
- 一方で、java には enum という機能がある。
enum
は、プログラム上は整数に近いが、人間向けの意味情報を標記の上で持たせることができる。- これが、
Direction
を導入しないといけない理由。
TriageLevel
の導入
Direction
とほぼ同じ理由。- これまでは、トリアージのレベルは、1,2,3 という数字で表されていた。
- プログラム中、1,2,3 と数値がでてきても、それがトリアージレベルであるということは、どうやってわかるのか。 わからなければ、プログラムの改変も、それが正しく動作しているかどうかも、分からない。
SetupFileInfo
の導入と、旧NetworkMap
の廃止。
- 各々のデータがどのクラスのインスタンスに含まれているかは、そのプログラムの動作や機能を規定する上で、一番大事な情報。
- これまでは、データの格納場所はかなり無秩序に決められ、クラスメイトの対応も全くなかった。
- 必要な情報を取り出すために、歪な呼び出しをあちこちで行う必要があった。
- なので、
NetworkMap
は、地図情報を保持することに専念し、 それ以外の情報はEvacuationSimulator
が親元となり、各々のまとまりで管理する構造に、とにかく変更。
RationalAgent
と RubyAgent
の導入
- CrowdWalk はマルチ「エージェント」シミュレーションを謳っている。
ならば、そのエージェントは理知的に振る舞えるようになっていなければならない。
- 何が「理知的」かは、もちろん研究課題。つまりそこは可変でなければならない。
- その「可変」を「プログラムの変更」による可変で実装するのは、
一番下手くそな方法。
- さらに、プログラムの改変を前提とすると、実験管理が非常に難しくなる。
- なので、「理知的」な部分は、プログラムからすれば「データ」として表されていなければならない。
- データとして表されていれば、実験管理がデータ管理として簡素化できる。
- 今回の実装では、
Term
による簡易プログラム表現をデータとして処理できるRationalAgent
と、 Ruby のプログラムとして「理知的」部分を表せるRubyAgent
を容易。RubyAgent
の場合は、Ruby のプログラムが必要だが、これは java のプログラムからすれば 外部データとして与えることになるため、実験管理がデータ管理できればいいだけになる。
CsvFormatter
の導入
- CrowdWalk が吐き出すログの大半は CSV 形式。その形式はかなり多様で複雑。
- 歴史的経緯か、非常に冗長に、かつ、無計画にカラムが追加されている。
- ヘッダーとなる行出力と、データ部の出力が全く違う場所にある。 データ部についても、状況によってことばる場所で出力していた。
- CSVの各カラムごとに、ヘッダー名と出力の方法をまとめてデータ化すれば良い。それが
CsvFormatter
。- メリットは2つ。
- 1つのカラムの定義を一箇所に集中でき、管理が簡潔。
- カラムの順序や取捨選択を、外部から制御できるようになる。
- もちろん、それ用のインターフェースを必要とするが、単なるデータ操作なので、実現は容易。
- メリットは2つ。