packages feed

crem-0.1.0.0: src/Crem/Decider.hs

{-# LANGUAGE DataKinds #-}

-- | The [Decider pattern](https://thinkbeforecoding.com/post/2021/12/17/functional-event-sourcing-decider)
-- allows to easily describe an [aggregate](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
-- in functional terms
--
-- In terms of Mealy machines, a `Decider` is a machine where the next state is
-- computed from the previous state and the output
module Crem.Decider where

import Crem.BaseMachine (ActionResult (..), BaseMachine, BaseMachineT (..), InitialState (..))
import Crem.Topology (AllowedTransition, Topology)
import Data.Foldable (foldl')
import "base" Data.Kind (Type)

-- | A @Decider topology input output@ is a Decider which receives inputs of
-- type @input@ and emits outputs of type @output@, where allowed transitions
-- are constrained by the provided @topology@.
--
-- Being used to describe the domain logic of an aggregate, a `Decider` is
-- always pure.
--
-- It is defined by:
--
--   * its `deciderInitialState`
--   * a `decide` function, which says how to compute the @output@ out of the
-- @input@ and the current state
--   * an `evolve` function, which allows us to specify the next state from the
-- current state and the @output@
data
  Decider
    (topology :: Topology vertex)
    input
    output = forall state.
  Decider
  { deciderInitialState :: InitialState state
  , decide :: forall vertex'. input -> state vertex' -> output
  , evolve
      :: forall initialVertex
       . state initialVertex
      -> output
      -> EvolutionResult topology state initialVertex output
  }

-- | A smart wrapper over the machine state, which allows to enforce that only
-- transitions allowed by the @topology@ are actually performed.
data
  EvolutionResult
    (topology :: Topology vertex)
    (state :: vertex -> Type)
    (initialVertex :: vertex)
    output
  where
  EvolutionResult
    :: AllowedTransition topology initialVertex finalVertex
    => state finalVertex
    -> EvolutionResult topology state initialVertex output

-- | translate a `Decider` into a `BaseMachine`
deciderMachine
  :: Decider topology input output
  -> BaseMachine topology input output
deciderMachine (Decider deciderInitialState' decide' evolve') =
  BaseMachineT
    { initialState = deciderInitialState'
    , action = \state input ->
        let
          output = decide' input state
         in
          case evolve' state output of
            EvolutionResult finalState ->
              ActionResult $ pure (output, finalState)
    }

-- | rebuild a `Decider` from a list of outputs
--
-- This is the main selling point of a `Decider` over a generic `Crem.StateMachine`,
-- since it allows rebuilding a machine from its outputs.
rebuildDecider
  :: [output]
  -> Decider topology input output
  -> Decider topology input output
rebuildDecider outputs decider =
  foldl' rebuildDeciderStep decider outputs
  where
    rebuildDeciderStep
      :: Decider topology input output
      -> output
      -> Decider topology input output
    rebuildDeciderStep (Decider (InitialState initialState') decide' evolve') output =
      let
        evolveResult = evolve' initialState' output
       in
        case evolveResult of
          EvolutionResult evolvedState ->
            Decider
              { deciderInitialState = InitialState evolvedState
              , decide = decide'
              , evolve = evolve'
              }