Just $ A sandbox

プログラミングと計算機科学とかわいさ

Object型とOpenUnion

今現在Haskellを使ってゲームを作っていて、そこで「オブジェクト」的なものが欲しくなってあれこれした結果を説明として残しておきたいので書きます。 Haskellオブジェクト指向をエミュレートするのには objective というのがあるんだけどまぁ大体そういう感じの話です。

Widget

ゲーム内では、Widgetと呼ばれる、画面上に表示されたり内部状態が変化して見た目が変わったりするコンポーネントを扱うことにしている。 つまり画面に表示されたりするUIを司るための型を用意しておいて、それに対して特定の信号を送ると内部状態が変化したりしなかったりする。

Widget型の定義は以下の通り

newtype Widget ops = Widget { runWidget :: forall m x. Monad m => Union ops m x -> pick (Widget ops) m x }

ops はoperatorのリストで、メソッドを集めてきたもの、と思えばいい。ただし各operatorは2つの型変数を持ち、カインドが (* -> *) -> * になっている。 Union ops m x はOpenUnion(ops の直和)で、データとしては {op m x : op `in` ops} だと思えばいい。 ここで m が出現していることで、後にこのWidgetが実行されるモナドをメソッドごとに変えられるようにしている。

pickWidget ops (自分自身)、 m (文脈)、 x (値) から実際に返すべき型を作るためのもの。

直和と直積

実際にゲーム内では pick として EitherT self m x つまり m (self + x) を選んでいるので、メソッドを呼ぶたびに自分自身を返すか値を返すかのいずれかができる。 pick が例えば pick self m x = m (x, self) の場合は大体objectiveのObject型と同じと思っていい。この場合はメソッドを呼ぶたびに自分自身と値のペアを返す。

大抵の場合はこの直和と直積のいずれかしか使う機会はないと思う。

一応違いとして、直和は常に内部状態を更新するのと値を返すのいずれかしか出来ないが、直積はどちらも必ず返す必要がある。 今回直和を選んだ理由として、値を返さないケース(メソッドチェインしたい時)にそうであることを明示する方法が直和にはあって直積にはないから、という感じだったけど 別に直積でもデータを捨てればいいのでどっちでもいいと思う。

内部状態

Widgetは「このメソッドが来たら」「内部状態を更新したり値を返したりする」ためのものだったけれど、当然内部状態を保持したいことがあると思う。 それは例えば次のようなループで書くと良い感じになる:

widget :: Widget [op1, op2]
widget = go inititalState where
  go :: State -> Widget _
  go state = Widget $ \case
    op1 -> go state'
    op2 -> go state''

内部状態は内部状態なので外からは見えないけれど、例えば内部状態を取り出すgetterやsetterを定義すれば いくらでも取り出したり変更したりはできる。

Widget Operation

Widget型で必要になるoperationは大体決まっていて、

Reset :: Widget ops -> Widget ops
Render :: Position -> Widget ops -> GameM ()
Run :: Widget ops -> GameM (Widget ops)
EventHandler :: KeyStates -> Widget ops -> GameM (Widget ops)

ぐらいを用意しておけばいいことにしてる。 これらの型は基本的にどんなwidgetでもあまり変わらないのでglobalに定義してexportしてる。

継承、合併

継承というか、2つのWidgetのメソッドを合併したWidgetというのが欲しい場合があって、単純には以下のようにすればいい。

union :: Widget [op1,op2] -> Widget [op3,op4] -> Widget [op1,op2,op3,op4]
union = go where
  go wx wy = Widget $ \case
    op1 -> wx @. op1
    op2 -> wx @. op2
    op3 -> wy @. op3
    op4 -> wy @. op4

しかしメソッドに被りがある場合、あるいは被りがあるかもしれない場合は重複を除く工夫が要る。 例えば明示的に直和を書くのも一つ。

union' :: Widget ops -> Widget ops' -> Widget (Sum ops ops')

-- このときメソッドは
-- InL op1
-- InR op3
-- のような形になる

そもそも、メソッドをただのリストとして持つのがいけない、という可能性もあって、本来ならば継承相当の機能を作るためには メソッドの間に順序っぽいものが定義されていたりすることが多いのでそういうのを上手く使って欲しいという気持ちもしないでもない。

というわけで無理やりどうにかする方法もなくはない:

union' :: Widget ops -> Widget [op1..opn] -> Widget (ops ++ [Lift op1 .. Lift opn])

力業感すごいけれどどちらかのメソッドを優先させて、優先されなかった方のメソッドもLiftで残すみたいなことも出来なくはない。 ただ最強にダサいのでどうにかして欲しい。

関係ないけれど

関係ないけれど、いわゆるeffect systemではこういうoperationが起きた順番を覚えておいて それが正しい順序で呼ばれているかを調べたりできるので、 そういうふうに使うことを考えると一般にメソッドのなす型はそれこそpreordered monoidぐらいにはなっていてほしいような気持ちもある。

リストじゃなくて自然にそういうpreorderが定義できるようなデータ構造で上のようなことを考えてやれば 継承も自然に定義できるじゃなかろうか。 と思って入るけどまだそれについてはちゃんと考えていない。

上でも言ったけれどWidgetのoperationは大体決まっているので、それを使ってこう良い感じに…みたいな。 困ったことにアイディアはなし。

おわりに

思ったよりobjectiveに寄せた感じの内容になってしまったけれど大体上のぐらいのものが用意されていると ゲーム制作には困らなさそうという実感があります。 現在はHaskellを2200行ぐらい書いてる模様ですが、特に破綻することなく苦しみもそんなになく普通に書けています。

でも、この記事を書いていてやっぱりリストで持つのはだめだなという考えに至りつつあるので そこも改善できたらしていきたい。 まぁあくまでゲーム制作が本来の目的なのでエターナらないようにだけは気をつけよう(自戒)。

ちなみに、この文書はorg-modeで書きました。