I'm playing around with some ideas about how to read aggregates from events.
From a high-level view I would love to have this:
class Apply ev ag where
apply :: ag -> ev -> ag
which to define how an Event ev
should change an Aggregate ag
if there is an instance of this class for the pair (if there is none then the aggregate should ignore the event).
If you look at the code below it's not hard at all to do it (please forgive me the anti-pattern with the Event
list there).
My problem starts when I want the events all serialized - or better: once I want them deserialized and applied again (to be honest: I don't care about the representations at all - but it should be possible to fold an aggregate from a heterogeneous list/stream of serialized events of course).
What I don't like about my solution is this ugly bit of boiler-plate:
instance DeserializableFor Person where
tryDeserialize (tn, sn)
| tn == typeOf (SetName "") = Just (App (read sn :: SetName))
| tn == typeOf (SetAge 0) = Just (App (read sn :: SetAge))
| otherwise = Nothing
which could probably be generated using TH but I would prefer a direct solution.
Any suggestions?
{-# LANGUAGE MultiParamTypeClasses, ExistentialQuantification #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Test where
import Data.Data
import Data.Maybe (catMaybes)
class Apply ev ag where
apply :: ag -> ev -> ag
data Applicable a = forall e . (Show e, Read e, Typeable e, Apply e a) => App e
type Events a = [Applicable a]
fold :: Events a -> a -> a
fold [] a = a
fold ((App e):es) a = fold es $ apply a e
-----------------------------------------
-- nasty solution
serialize :: Events a -> [(TypeRep, String)]
serialize = map (\ (App e) -> (typeOf e, show e))
-- is there a way to get something like this from the `Apply` contexts?
class DeserializableFor a where
tryDeserialize :: (TypeRep, String) -> Maybe (Applicable a)
-- because THIS is really ugly
instance DeserializableFor Person where
tryDeserialize (tn, sn)
| tn == typeOf (SetName "") = Just (App (read sn :: SetName))
| tn == typeOf (SetAge 0) = Just (App (read sn :: SetAge))
| otherwise = Nothing
deserialize :: DeserializableFor a => [(TypeRep, String)] -> Events a
deserialize = catMaybes . map tryDeserialize
--------------------------------------------------------
-- Example
data SetName = SetName String
deriving (Show, Read, Typeable)
data SetAge = SetAge Int
deriving (Show, Read, Typeable)
data SomeOtherEvent = SomeOtherEvent
deriving (Show, Read, Typeable)
data Person = Person { name :: String, age :: Int }
deriving Show
nobody :: Person
nobody = Person "" 0
instance Apply SetName Person where
apply p (SetName n) = p { name = n }
instance Apply SetAge Person where
apply p (SetAge a) = p { age = a }
events :: Events Person
events = [App (SetName "Mr.Wrong"), App (SetAge 42)]
serialized :: [(TypeRep, String)]
serialized = (typeOf SomeOtherEvent, show SomeOtherEvent) : serialize events
person :: Person -- is `Person {name = "Mr.Wrong", age = 42}`
person = fold (deserialize serialized) nobody
Apply
constraintApplicable a
is isomorphic to functiona -> a
. When I have more time I'll try to come up with an alternative proposal. The idea is that as long as Events are in the same stream/storage, just keep them asByteString
s or so. And once you need them typed, deserialize just those that match the corresponding aggregate. \$\endgroup\$