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