Haskell でオブジェクト指向 (1)

後輩の記事
モナドを勉強してみたので、オブジェクトっぽいのを作ってみた。 - Milestones to EVERPEACE 〜alius via〜
に触発されて、ここ半月ほど Haskell にはまっている。
記事自体とても興味深かったんだけど、読み終わった後のモヤモヤ感がなかなか払拭できなかった。たぶん、自分が思っているオブジェクトの概念が実装されていないからだと思う。
自分としては、以下の概念が実装されていてほしい。

というわけで、修正バージョンを作成してみた。
まずは Person 型から。後輩のサンプルだと、戻り値(?)が Obj になってるけれど、素直に Person にしておく。

data Person =
  Person {
    name :: String,
    age  :: Int
  }
  deriving Show

setName :: String -> Person -> Person
setName newName (Person _ age) =
  Person newName age

setAge :: Int -> Person -> Person
setAge newAge (Person name _) =
  Person name newAge

Obj は値ではなく ID を保持することを強調して Entity に変更して実装。
状態を変更すると、ID は変化せず、バージョンが 1 増加する。
Functor を使うことで、Person と Entity が分離できた。

data Entity a =
  Entity {
    id      :: Integer,
    version :: Integer,
    state   :: a
  }
  deriving Show

instance Functor (Entity)
  where
    fmap f (Entity id version x) =
      Entity id (version + 1) (f x)

initialVersion :: Integer
initialVersion = 1

makeEntity :: a -> IdGen -> (Entity a, IdGen)
makeEntity x g =
  let
    (id, g') = nextId g
  in
    (Entity id initialVersion x, g')

updateEntity :: a -> Entity a -> Entity a
updateEntity newX (Entity id version _) =
  Entity id (version + 1) newX

エンティティ生成時に、ID の自動採番は IdGen で行います。

data IdGen =
  IdGen {
    lastId :: Integer
  }
  deriving Show

nextId :: IdGen -> (Integer, IdGen)
nextId g =
  let
    id = (lastId g) + 1
  in
    (id, IdGen id)

最後に、メインはこんな感じ。モナドがまだ十分理解できていないので、まだまだ修正の余地があると思いますが。

main =
  let
    g0       = IdGen 1000
    (x1, g1) = makeEntity (Person "Foo" 12) g0
    x2       = fmap (setName "Bar") x1
    x3       = fmap (setAge 23) x2
    (y1, g2) = makeEntity (Person "Hoge" 34) g1
    y2       = updateEntity (Person "Fuga" 45) y1
  in do
    print g0
    print g1
    print x1
    print x2
    print x3
    print g2
    print y1
    print y2

実行すると、こんな結果になります。

prompt>runghc entity-example0.hs
IdGen {lastId = 1000}
IdGen {lastId = 1001}
Entity {id = 1001, version = 1, state = Person {name = "Foo", age = 12}}
Entity {id = 1001, version = 2, state = Person {name = "Bar", age = 12}}
Entity {id = 1001, version = 3, state = Person {name = "Bar", age = 23}}
IdGen {lastId = 1002}
Entity {id = 1002, version = 1, state = Person {name = "Hoge", age = 34}}
Entity {id = 1002, version = 2, state = Person {name = "Fuga", age = 45}}

いかがでしょうか? ID を保ったまま、状態が変化していくイメージが実装できたのではと思います。
今回、モナドらしさはあまり表現されていませんが、モナドに対する認識にちょっと変化があった。当初、参照透過性を保ちつつ状態変化をシミュレートするのにモナドが活躍するのかと思っていたが、いろいろ調べてみると、関数型言語の仕組みを手続き型っぽく見せるためにモナドが活躍するけれど、状態変化はモナドとは別に設計しないといけないんじゃないかと思った。それを手続き型ふうに書く手助けとしてモナドは使えると思うけれど。
この認識であってる!?

そんなわけで、もう少しモナドを勉強して、Haskell で手続き型ふうのオブジェクト指向を極めたいと思います。