FsUno.Prod


2014-08-11

Designing Aggregates, Algebra

This evening, I start to feel that things won't move much as long as I have only one Aggregate.

With several Aggregates we'll have to add Process and Process Managers to orchestrate the reactions to events, and ensure the global consistency of the system.

We'll also have to manage projections over several aggregates. And make the infrastructure code a bit more generic to not wire it directly to the only aggregate.

But before this happens: Design !

The Game Aggregate

This one is already partly implemented and represents the enforcement of rules on the discard pile.


A discussion on Jabbr with @rbartelink interrupted me in the process for something awesome !

Algebra

The first question was whether to change the empty member and make it a static member, ditto the apply function.

The second was whether to try to hide empty and apply completly in a replay function.

empty as a static member feels quite natural, but I had a problem with apply. Its signature seems to validate this change, but putting it before handle seems wrong.

The problem is that when reading an Aggregate, the first important thing is the decision in the handle method, and changes in apply are always implemented later. Changing this would break the flow.

But it's possible to implement it further in F# with a type extension:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
    type State = { CurrentState: string }
        with
        static member empty = { CurrentState = "" }

    let handle command state =
        /// ...
        [ SomethingHappened ]

    type State with
        static member apply state = function
            | SomethingHappened -> { state with CurrentState = "new state" }

The second question started with this sample code:

1: 
2: 
        static member build = List.fold State.apply  
        static member replay = State.build State.empty 

It would be possible to write this but it's not part of the Aggregate, let's see why.

Monoids

Do you remember how a Monoid is defined ?

A monoid is a triple (S,+,n) where

  • S is a set
  • + is closed on the set (the signature would be S -> S -> S)
  • + is associative ( x + y + z = (x + y) + z = x + (y + z) )
  • n is the neutral(aka identity) element ( x + n = n + x = x)

Most of mathematical structures are defined using sets (here S), functions (here +) and specific set items (here n).

Aggregates

Aggregates are not monoids, but you can see that it could be defined using the same kind of primary element:

A quadruple (State, handle, apply, empty) with the rules and signatures you know. handle and apply are not closed on the set but the composition is not bad.

If we alias apply as + we can write:

1: 
2: 
3: 
    let (+) = State.apply

    let finalState = State.empty + SomethingHappened + SomethingHappened + SomethingHappened

Makes sense, right ?

The only change that I would propose is to change empty to initial because it's the initial state:

1: 
2: 
3: 
4: 
    type State with
        static member initial = { CurrentState = "initial state"}

    let finalState' = State.initial + SomethingHappened + SomethingHappened

Hiding apply and initial would seriously reduce the composability of all this.

Higher order functions

replay can actually be seen as a high order function built using initial and apply. To build it directly from the State type we can use statically resolved type parameters, you can hover over the replay definition below to see the type constraints:

1: 
2: 
3: 
4: 
    let inline replay events =
        let initial = (^S: (static member initial: ^S) ()) 
        let apply s = (^S: (static member apply: ^S -> (^E -> ^S)) s)
        List.fold apply initial events

Ok, it's a bit more complicated but you can check the signature by hovering the function, and it can be used on any type that has initial and apply members that adhere to the expected signature. And the inference still works from the return type, so any hint to the compiler indicating the specific Aggregate State type in use means there is no need for further type annotations.

1: 
2: 
3: 
    let state : State =
        [ SomethingHappened; SomethingHappened]
        |> replay

So where are we now ? Better vocabulary with initial, better understanding about the Aggregate algebra, and a high order function to use it.

Now we can see how we'll implement more generic command handlers.

Good evening !

union case Command.DoSomething: Command
Multiple items
module Event

from Microsoft.FSharp.Control

--------------------
type Event = | SomethingHappened

Full name: Designing Aggregates.Event

--------------------
type Event<'T> =
  new : unit -> Event<'T>
  member Trigger : arg:'T -> unit
  member Publish : IEvent<'T>

Full name: Microsoft.FSharp.Control.Event<_>

--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
  new : unit -> Event<'Delegate,'Args>
  member Trigger : sender:obj * args:'Args -> unit
  member Publish : IEvent<'Delegate,'Args>

Full name: Microsoft.FSharp.Control.Event<_,_>

--------------------
new : unit -> Event<'T>

--------------------
new : unit -> Event<'Delegate,'Args>
union case Event.SomethingHappened: Event
module FirstAttempt

from Designing Aggregates
type State =
  {CurrentState: string;}
  static member apply : state:State -> (Event -> State)
  static member build : (State -> Event list -> State)
  static member empty : State
  static member initial : State
  static member replay : (Event list -> State)

Full name: Designing Aggregates.FirstAttempt.State
State.CurrentState: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
static member State.empty : State

Full name: Designing Aggregates.FirstAttempt.State.empty
val handle : command:'a -> state:'b -> Event list

Full name: Designing Aggregates.FirstAttempt.handle
val command : 'a
val state : 'b
static member State.apply : state:State -> (Event -> State)

Full name: Designing Aggregates.FirstAttempt.State.apply
val state : State
static member State.build : (State -> Event list -> State)

Full name: Designing Aggregates.FirstAttempt.State.build
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val fold : folder:('State -> 'T -> 'State) -> state:'State -> list:'T list -> 'State

Full name: Microsoft.FSharp.Collections.List.fold
static member State.apply : state:State -> (Event -> State)
static member State.replay : (Event list -> State)

Full name: Designing Aggregates.FirstAttempt.State.replay
property State.build: State -> Event list -> State
property State.empty: State
val finalState : State

Full name: Designing Aggregates.FirstAttempt.finalState
static member State.initial : State

Full name: Designing Aggregates.FirstAttempt.State.initial
val finalState' : State

Full name: Designing Aggregates.FirstAttempt.finalState'
property State.initial: State
val replay : events:'E list -> 'S (requires member get_initial and member apply)

Full name: Designing Aggregates.FirstAttempt.replay
val events : 'E list
val initial : 'S (requires member get_initial and member apply)
val apply : ('S -> 'E -> 'S) (requires member get_initial and member apply)
val s : 'S (requires member get_initial and member apply)
val state : State

Full name: Designing Aggregates.FirstAttempt.state
Fork me on GitHub