Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating Generic Typeclasses in Haskell

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
like image 411
mclark1129 Avatar asked Feb 04 '23 06:02

mclark1129


1 Answers

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:

  • do I intend to write useful code that is polymorphic in this class?
  • do I expect users to define their own instances I haven't thought of?
  • does it have any laws?

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.

like image 83
jberryman Avatar answered Feb 16 '23 04:02

jberryman