As an exercise to wrap my brain around Haskell types and typeclasses, I'm attempting to implement a simple DDD/CQRS style solution. I'm modelling it directly after Lev Gorodinski's F# Simple CQRS implementation.
I've implemented a very simple Vehicle
aggregate, which is more or less a direct port of Lev's InventoryItem
aggregate
module Vehicle where
data State = State { isActive :: Bool } deriving (Show)
zero = State { isActive = True }
type Mileage = Int
data Command =
Create String Mileage
| UpdateMileage Mileage
| Deactivate
deriving (Show)
data Event =
Created String
| MileageUpdated Mileage
| Deactivated
deriving (Show)
-- Define transitions from a command to one or more events
execute :: State -> Command -> [Event]
execute state (Create name mileage) = [Created name, MileageUpdated mileage]
execute state (UpdateMileage mileage) = [MileageUpdated mileage]
execute state Deactivate = [Deactivated]
-- Apply an event against the current state to get the new state
apply :: State -> Event -> State
apply state (Created _) = state
apply state (MileageUpdated _) = state
apply state Deactivated = state { isActive = False }
The part that I'm trying to figure out is how to create a higher level abstraction for the domain aggregate, since all Aggregates would be made up of the same component parts. In Lev's example, he defined a type Aggregate<'TState, 'TCommand, 'TEvent>
which allowed him to define a generic command handler which would work against any instance of a domain Aggregate. In Haskell this feels like something I should be using Typeclasses for. Of course, since I have no idea what I'm doing this may be a complete misconception of what they are used for.
My thought is that an Aggregate
Typeclass would define the interface for the execute
and apply
commands, as well as somehow that the type requires associated types to represent its State, Commands, and Events. From there I could define some generic command handler that would be able to execute a command against any instance of an Aggregate. For example
class Aggregate a where =
execute :: state -> command -> [event]
apply :: state -> event -> state
Where state
, command
, and event
are type variables representing State, Command, and Event for a given instance of Aggregate
. This doesn't work, of course.
Is what I'm trying to do an appropriate use of Typeclasses? If so, how should I define the class such that an instance of Aggregate
must have corresponding State, Command, and Event types?
If this the wrong approach, how should I be defining Aggregate
in order to create higher level abstractions?
Update
Following @jberryman's advice I used an MPTC to define my Aggregate, which allowed me to create the generic command handler I was looking for:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
module Aggregate where
class Aggregate state command event
| state -> command event, command -> state event, event -> state command where
execute :: state -> command -> [event]
apply :: state -> event -> state
zero :: state
makeHandler (load, commit) =
(\ (id, expectedVersion) command ->
do events <- load id
let state = foldl apply zero events
let newEvents = execute state command
commit (id, expectedVersion) newEvents)
Which in turn leads to the following instance declaration in my Vehicle
module
instance Aggregate State Command Event where
execute = Vehicle.execute
apply = Vehicle.apply
zero = Vehicle.zero
And then a sample script tying it all together:
import Aggregate
import Vehicle
-- Mock out IO to a domain repository (EventStore)
load :: Int -> IO [Event]
load id = do return [Created "Honda", MileageUpdated 15000]
commit (id, expectedVersion) events = putStrLn (show events)
-- Create the handler provide a command to it
handler = makeHandler (load, commit)
handler (1,1) Deactivate
If state
, command
and event
are a triple that are all unique to a particular instance you can use MultiParameterTypeClasses
along with FunctionalDependencies
like
class Aggregate state command event | state -> command event, command -> state event, event -> state command where
execute :: state -> command -> [event]
apply :: state -> event -> state
The fundeps after the |
read: "where command and event are uniquely determined by state, and state and event are uniquely determined by command, and..." etc. This allows instances to be resolved if we can infer any one of the types in the instance head. You could do the same with type families.
But three questions you should ask yourself before defining a type class:
If the answer is no to all of these then you probably shouldn't be defining a type class. It's hard to give an answer as to whether that's the case here.
But it might be e.g. that what you really want is something like:
data Aggregate state command where
Aggregate :: (state -> command -> [event]) -> (state -> event -> state) -> Aggregate state command
this is a GADT where event
is hidden.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With