- Published on
Functions (and facts) describe the world
- Authors
- Name
- Damian Płaza
- @raimeyuu
This tale is a tiny contribution to the great F# Advent Calendar 2024.
Take your time and check other contributions too (of course, after reading this tale)!
This time, I want to express thankfulness to two people:
- big thanks to borar (again!) for inspiring conversations and functional teachings
- big thanks to Oskar Dudycz for inspiring conversations, architectural (and more!) insights and for being a great person to talk to
Both borar and Oskar Dudycz had an interesting discussion that somewhat inspired me to write something about the topic.
The title of this tale refers to a great video.
Aggregates?
In one of my previous tales, prepared specifically for F# Advent Calendar 2022, we took a tour around representing "an aggregate", a Domain-Driven Design tactical pattern, in variety of ways (see Many faces of DDD Aggregates in F#).
In the next tale, made for F# Advent Calendar 2023, we went down into the rabbit hole of modeling state machines using functional-first thinking (see FSM - Functional State Machines).
In fact, both of those tales were about enforcing a set of rules so that the state of a part of the system stays transactionally consistent (whatever it actually means).
Of course, I wrote them having in mind that they must be immediately shipped to production in one of your critical service providers, dear Reader.
If you know me a bit (I hope, dear Reader), you probably understand that little sarcarsm.
Both of those tales had exploratory nature - we were able to had playful moments with various representations of a concept of "an aggregate".
Ah, sweet aggregate of mine - everyone want to have you in one's system, right?
But let's get back to the main point of "aggregates" - "enforcing a set of rules so that the state of a part of the system stays transactionally consistent".
The world beyond "aggregates"
Turns out that both of those tales had something in common (and it wasn't only a concept of "an aggregate") - we focused very much of rules enforcement (also called "invariants"), completely ignoring the output of application of the knowledge regarding those rules (application of the knowledge? You might be interested in The ambiguity of application).
As a reminiscence, here are two code snippets (slightly modified) from the tales:
let ignoreFacts = snd // built-in function!
let shift = BouncersShift.start ["mike";"john";"derek"]
shift
|> BouncersShift.startGuarding "mike"
|> (ignoreFacts >> BouncersShift.startGuarding "derek")
// \________________ each operation yields both facts that happened
// during execution and current state machine
let discardOutput = snd
doorMechanism
|> DoorMechanism.send Open
>>= (discardOutput >> DoorMechanism.send Close)
>>= (discardOutput >> DoorMechanism.send Lock)
>>= (discardOutput >> DoorMechanism.send Open)
// \________________ each operation yields both facts that happened
// during execution and current state machine
One could say that "an aggregate" is "just" something that aggregates the rules to enforce consistent state change - so it can be nicely represented as a (functional) state machine.
We saw that, right dear Reader?
But it is only one (tactical) "pattern" in the arsenal of Domain-Driven Designing engineer.
There are other building blocks that we can employ to build our systems using Domain-Driven Design approach (at least the "tactical side" of it), depending on the problem area we are building software solutions for (right?).
In this tale, instead of "ignoring facts" or "discarding output", we are going to utilize those facts to build toy system, using other "conceptual primitives" from a "tactical" DDD and and Event Storming toolbelts.
Other building blocks?
Facts?
Let's go!
Aggregates, state machines and...Functions?!
As I was doing a research for this tale, I stumbled upon a really interesting resource.
In his blog post, the author discusses using Mealy machines to model aggregates and...Projections and policies.
We used Mealy state machines to model aggregates, but projections and policies?!
That made me really, really curious.
I enjoyed all the insightful observations the author made, especially that he focused on the compositional aspect of Mealy state machines (say hi to Category Theory).
Also, all concepts were represented by...Functions.
As he omitted the implementation details, I decided to take a step further and implement a fully-functioning toy example (sounds like a paradox, isn't it?), using the knowledge and observations from the post.
Firstly, we need to revisit the concept of "an aggregate".
Wait, design decisions ahead!
But before that, a small design decision.
The author deliberately used Mealy machines to represent the "tactical DDD" and Event Storming building blocks.
As we already saw how to use Mealy state machines, to make everything a little bit simpler, we're going to drop "halting" aspect of the Mealy machines and assume that our state machines are going to work forever.
This means that instead of such state machine encoding:
type Step<'Output, 'Continue> =
| Stop // 👈 this enables "halting"
| ContinueWith of ('Output * 'Continue)
type Mealy<'Input, 'Output> = Mealy of ('Input -> Step<'Output, Mealy<'Input,'Output>>)
We're going to use the following finite-state machine representation:
type FSM<'Input, 'Output, 'State> =
FSM of ('State * ('State -> 'Input -> 'Output))
Dropping Stop
case means that we would work with a single-case union, keeping output and continuation.
It is an output of a processing and a function that does this processing.
Additionally, we're going to use a signature for a function to evolve FSM
:
type Evolve<'Input, 'Output, 'FSM> = 'Input -> 'FSM -> 'Output * 'FSM
There's no type constraint on 'FSM
for sake of simplicity.
Wait, a domain?
Imagine that we are producing high-tech doors, full of sensors and actuators. One is able to interact with a single instance of a door remotely, using a handy app. Thanks to our innovative approach to door manufacturing, we are able to track various interactions with physical doors, along with triggering actions on them.
This means, we're able to continue a domain that was introduced in FSM-Functional State Machines tale - a DoorMechanism
.
Additionally, we're going to extend it with a concept of "door knocking", introduced in the mentioned article.
To do so, we need to think not only about "mechanism" itself, but about the door as a whole (to allow "knocking").
Let's sum up the rules to maintain the integrity of a door's representation:
- door starts as
Open
- only when it's
Closed
, when trying toOpen
it, it becomesOpen
- only when it's
Opened
, when trying toClose
it, it becomesClosed
- only when it's
Closed
, when trying toLock
it, it becomesLocked
- only when it's
Locked
, when trying toUnlock
it, it becomesClosed
- when it's
Closed
, it can beKnocked
and it remainsClosed
- when it's
Locked
, it can beKnocked
and it remainsLocked
To make our understanding better, let's draw it:
As mentioned before, we're going to represent three concepts:
- an aggregate
- a policy
- a projection
In the original article, "Picture that explains (almost) everything" is mentioned:
We are going to use it to visualize what part of this diagram we are focusing on at the moment of exploration.
To achieve so, we're going to use the following, simplified, picture:
Phew, that was a bit exhausting, wasn't it?
Before starting our exploratory journey, we needed to set the frame, but finally we are ready.
Buckle up, we have things to play with!
First function - "an aggregate"
Ok, time to encode a concept of "an aggregate"!
type Aggregate<'Command, 'Event, 'State> =
FSM<'Command, 'Event list, 'State>
Now let's use it to represent concepts from our domain:
module Door =
type DoorState = private DoorOpen | DoorClosed | DoorLocked
type DoorCommand = private Open | Close | Knock | Lock | Unlock
type DoorEvent = private Opened | Closed | Knocked | Locked | Unlocked
type Door = Door of Aggregate<DoorCommand, DoorEvent, DoorEvent list>
// |
// |
// |__ it can be thought of as:
// FSM (DoorEvent list, DoorState -> DoorCommand -> DoorEvent list * DoorState)
type EvolveDoor = Evolve<DoorCommand, DoorEvent list, Door>
// |
// |__ it can be thought of:
// DoorCommand -> Door -> DoorEvent list * Door
As before, we want to express the language of the domain, hence we use single-case union Door
to communicate "aggregate's" intention.
(an avid Domain-Driven Design Reader might notice that naming your "aggregates" like that might be considered an "anti-pattern", but I am adult so I know what I am doing, right?)
We have the basic structure, let's focus on evolving our "aggregate state machine":
let evolve: EvolveDoor =
// DoorCommand -> Door -> DoorEvent list * Door
fun command aggregate ->
let (Door (FSM (events, applyTo))) = aggregate // 👈 destructuring to get to "state" and "deciding function"
let applyTo' _ command = command |> applyTo events // 👈 for "better" readability
let newEvents = command |> applyTo' aggregate // 👈 as we saw on the diagram
newEvents, Door(FSM((events |> List.append newEvents), applyTo)) // 👈 returning "output" and appending new events to the history
We're going to start with something we already know - state transitions:
module DoorState =
let applyTo state events =
// DoorState -> DoorEvent list -> DoorState
events |> List.fold applyTo' state
let private applyTo' state event = // 👈 evolving "state" with a single event
// DoorState -> DoorEvent -> DoorState
match state, event with
| DoorOpen, Closed -> DoorClosed
| DoorOpen, _ -> DoorOpen
| DoorClosed, Opened -> DoorOpen
| DoorClosed, Locked -> DoorLocked
| DoorClosed, _ -> DoorClosed
| DoorLocked, Unlocked -> DoorClosed
| DoorLocked, _ -> DoorLocked
That was pretty straightforward, wasn't it?
We are able to evolve a DoorState
by utilizing all DoorEvent
s that happened before.
But "an aggregate" is something else than a state. It is able to transform a command into a list of events, here we dealt "just" with state transitions.
In fact, Mealy machines express both state transitions and output, provided with each transition.
This fact (pun intended) - outputs - weren't captured in previous tales because we completely ignored the them, but now the story is different - this is our main star (next to functions, of course).
Let's fix our FSM chart:
Of course, we used our deep domain knowledge to express what events happen for a given state transition, knowing the incoming command.
And this is our applyTo
we were missing!
let applyTo state command =
// DoorEvent list -> DoorCommand -> DoorEvent list
let state = events |> DoorState.applyTo DoorOpen // 👈 we "build" door state from each events that happened
let newEvents =
match state, command with // 👈 we're deciding what events happen after a certain command is applied to a "state"
| DoorOpen, Close -> [Closed]
| DoorOpen, _ -> []
| DoorClosed, Knock -> [Knocked]
| DoorClosed, Open -> [Opened]
| DoorClosed, Lock -> [Locked]
| DoorClosed, _ -> []
| DoorLocked, Unlock -> [Unlocked]
| DoorLocked, Knock -> [Knocked]
| DoorLocked, _ -> []
newEvents
It seems we have almost everything in place to play with our "aggregate".
module Door =
let noInitialEvents = List.empty<DoorEvent>
let door() = Door(FSM(noInitialEvents, applyTo))
let close = evolve Close
let open' = evolve Open
let knock = evolve Knock
let lock = evolve Lock
let unlock = evolve Unlock
Here is the public API to work with "a door".
Up to now, it should look familiar (if you had chance to see previous tales, dear Reader).
Ok, no more waiting - let's interact with a representation of "a door" (in fact, just one aspect of a door, because the rest, like weight, height, price, isn't in the area of our interest!).
let aDoor = Door.door()
let aDoor' =
aDoor
|> (Door.close >> andPublish)
|> (Door.knock >> andPublish)
|> (Door.knock >> andPublish)
|> (Door.lock >> andPublish)
|> (Door.knock >> andPublish)
|> (Door.knock >> andPublish)
|> (Door.unlock >> andPublish)
For the sake of this tale, let's assume that such sequence - close, knock, knock, lock, knock, knock, unlock
- will be used from now on when interacting with the door.
If we run it in FSI, then we get something like this:
val aDoor: Door.Door = Door (FSM ([], <fun:aDoor@357-8>))
val aDoor': Door.Door =
Door (FSM ([Closed; Knocked; Knocked; Locked; Knocked; Knocked; Unlocked], <fun:aDoor@357-8>))
Ok, this time we have a new player in the game - andPublish
function:
let andPublish, subscribeToDoorEvents =
Messaging.InMemory.doorEventBroker()
It is a simple implementation of a "publish-subscribe" pattern, where we can subscribe to events and publish them.
Of course it is production-grade as it is in-memory.
Now, it's time to subscribe to those events and see the history of our door's life:
subscribeToDoorEvents(fun event ->
printfn $"A door has been {event}"
)
After subscribing and interacting with the door's representation, we get:
A door has been Closed
A door has been Knocked
A door has been Knocked
A door has been Locked
A door has been Knocked
A door has been Knocked
A door has been Unlocked
Here is a dumb simple implementation of doorEventBroker
:
let doorEventBroker() =
broker<_, _, _>( // 👈 we're using a generic broker and we completely let the Almighty F# Compiler do the heavylifting for us
fun result subscriber ->
let events, _ = result // 👈 we're destructuring the result of evolving FSM and use events
events |> List.iter subscriber // 👈 we're letting subscriber know about new events
)
Beware, here comes the most complex code in this tale!
[<RequireQualifiedAccess>]
module Messaging =
[<RequireQualifiedAccess>]
module InMemory =
let broker<'OutputToPublish, 'SubscriberInput, 'FSM>
(createPublishTo: 'OutputToPublish * 'FSM -> ('SubscriberInput -> unit) -> unit) =
let mutable subscribers = List.empty<'SubscriberInput -> unit>
let subscribe (subscriber: 'SubscriberInput -> unit) =
subscribers <- subscriber :: subscribers // 👈 we maintain in-memory list of subscribers
let publish (outputToPublish: 'OutputToPublish, fsm: 'FSM) =
let publishTo = createPublishTo (outputToPublish, fsm)
List.iter publishTo <| subscribers
fsm // 👈 we want to return FSM for pipe-friendly processing
publish, subscribe // 👈 our public API
So this little function creates an in-memory list of subscribers and allows to publish an output from FSMs to them.
Dumb simple.
Wouldn't it be nice to use events for something else than just printing them?
Let's move on to policies!
Second function - "a policy"
So according to the diagram, it is driven by events and it produces commands.
type Policy<'Event, 'Command, 'PolicyState> =
FSM<'Event, 'Command, 'PolicyState>
In the mentioned article, the author does not provide any state for the policy, so it is stateless.
Let's try the same.
Here's the policy structure:
module Policies =
module EachKnockNotifies =
type EachKnockNotifiesCommand =
SendSms | NoAction
type EachKnockNotifiesPolicy =
EachKnockNotifies of Policy<DoorEvent, EachKnockNotifiesCommand, unit>
type EvolveKnockNotifiesPolicy =
Evolve<DoorEvent, EachKnockNotifiesCommand, EachKnockNotifiesPolicy>
It means that we will have no state (unit
) and as an output we are going to give EachKnockNotifiesCommand
back.
We wanted to encode "complete" problem space, so in case other event than Knocked
happened, we want to express "no action" - hence NoAction
.
This also makes our policy "pure" - it just describes the reaction to the event, and execution might happen later (as "someone" interprets the result).
As you noticed, dear Reader, I already gave a name to your policy so we could state the policy in the natural language as: "whenever knocked then send SMS".
As our policy is stateless, we can use a simple function to evolve it:
let applyTo () event =
// unit -> DoorEvent -> EachKnockNotifiesCommand
match event with // 👈 we are making a decision how to react on events
| Knocked -> SendSms
| _ -> NoAction
let evolve: EvolveKnockNotifiesPolicy =
// DoorEvent -> EachKnockNotifiesPolicy -> EachKnockNotifiesCommand * EachKnockNotifiesPolicy
fun event policy ->
let (EachKnockNotifies(FSM(noPolicyState, applyTo))) = policy
let command = event |> applyTo noPolicyState // 👈 here is our stateless "policy"
command, policy
let eachKnockNotifiesPolicy() = EachKnockNotifies(FSM((), applyTo));
As we have our policy in place, let's react to door events:
let eachKnockNotifiesPolicy = Door.Policies.EachKnockNotifies.eachKnockNotifiesPolicy() // 👈 in-memory representation of our policy
subscribeToDoorEvents(fun event ->
let command, _ = eachKnockNotifiesPolicy |> EachKnockNotifies.evolve event // 👈 policy is stateless so we discard the FSM
printfn $"{event} triggers {command}"
)
If we run everything in FSI, we shall get:
// as a reminder, sequence of door interactions we ran:
// close, knock, knock, lock, knocked, knocked, unlock
EachKnockNotifiesPolicy: Closed triggers NoAction
EachKnockNotifiesPolicy: Knocked triggers SendSms
EachKnockNotifiesPolicy: Knocked triggers SendSms
EachKnockNotifiesPolicy: Locked triggers NoAction
EachKnockNotifiesPolicy: Knocked triggers SendSms
EachKnockNotifiesPolicy: Knocked triggers SendSms
EachKnockNotifiesPolicy: Unlocked triggers NoAction
It's quite strange to send SMS each time someone knocks on the door, isn't it?
Imagine we want to be notified after someone knocked the door every three times.
This means that our policy is no longer stateless, as we need to count how many times the fact of knocking happened.
Time to model!
module Policies =
module ThreeKnocksNotification =
type ThreeKnocksNotificationCommand = SendSms | NoAction
type KnocksCount = KnocksCount of count: int
type SendingSmsOnThreeKnocksPolicy =
SendingSmsOnThreeKnocksPolicy of Policy<DoorEvent, KnocksCount * ThreeKnocksNotificationCommand, KnocksCount>
type EvolveSendingSmsOnThreeKnocksPolicy =
Evolve<DoorEvent, ThreeKnocksNotificationCommand, SendingSmsOnThreeKnocksPolicy>
Now time to apply our knowledge to evolve the policy:
let applyTo policyState event =
// KnocksCount -> DoorEvent -> KnocksCount * ThreeKnocksNotificationCommand
let (KnocksCount knocksCount) =
event |> ThreeKnocksNotificationPolicyState.applyTo policyState
let command =
match event, knocksCount with // 👈 we decide how to react to door event that happened before
| Knocked, knocksCount' when knocksCount' % 3 = 0 -> SendSms
| _, _ -> NoAction
(KnocksCount knocksCount), command
let evolve: EvolveSendingSmsOnThreeKnocksPolicy =
// DoorEvent -> SendingSmsOnThreeKnocksPolicy -> ThreeKnocksNotificationCommand * SendingSmsOnThreeKnocksPolicy
fun event policy ->
let (SendingSmsOnThreeKnocksPolicy(FSM(policyState, applyTo))) = policy
let knocksCount, command = event |> applyTo policyState // 👈 now "a policy" deals with its state
command, SendingSmsOnThreeKnocksPolicy(FSM(knocksCount, applyTo))
let sendingSmsOnThreeKnocksPolicy () = SendingSmsOnThreeKnocksPolicy(FSM((KnocksCount 0), applyTo))
module ThreeKnocksNotificationPolicyState =
let applyTo policyState event =
// KnocksCount -> DoorEvent -> KnocksCount
let (KnocksCount knocksCount) = policyState
match event, knocksCount with // 👈 we decide how to evolve "policy state"
| Knocked, knocksCount' -> (KnocksCount (knocksCount' + 1))
| _, _ -> (KnocksCount knocksCount)
Ok, we have all in place to subscribe to the events and react to them!
let knocksPolicyRepository =
InMemory.repositoryFor(Door.Policies.ThreeKnocksNotificationPolicy.sendingSmsOnThreeKnocksPolicy) // 👈 in-memory representation of our policy
subscribeToDoorEvents(fun event ->
let currentPolicyState = knocksPolicyRepository.read() // 👈 we're reading the current state of the policy
let command, newPolicyState =
Door.Policies.ThreeKnocksNotificationPolicy.evolve event currentPolicyState // 👈 evolving the policy
printfn $"ThreeKnocksNotificationPolicy: {event} triggers {command}" // 👈 here we are executing the result of "a reaction" to event
knocksPolicyRepository.save newPolicyState // 👈 we're saving the new state of the policy
)
If we run everything in FSI, this is the output we should see:
// as a reminder, sequence of door interactions we ran:
// close, knock, knock, lock, knocked, knocked, unlock
ThreeKnocksNotificationPolicy: Closed triggers NoAction
ThreeKnocksNotificationPolicy: Knocked triggers NoAction
ThreeKnocksNotificationPolicy: Knocked triggers NoAction
ThreeKnocksNotificationPolicy: Locked triggers NoAction
ThreeKnocksNotificationPolicy: Knocked triggers SendSms
ThreeKnocksNotificationPolicy: Knocked triggers NoAction
ThreeKnocksNotificationPolicy: Unlocked triggers NoAction
It seems that we are going to be notified only after the third knock.
One thing we need to reveal is the implementation of repositoryFor
.
An avid Reader might notice that this is a in-memory implementation of a Repository
pattern.
module Persistence =
type Repository<'FSM> =
abstract member read: unit -> 'FSM
abstract member save: 'FSM -> unit
[<RequireQualifiedAccess>]
module InMemory =
let repositoryFor<'FSM> (initialize: unit -> 'FSM) = // 👈 deferred initialization
let mutable fsm = initialize()
let read () = fsm
let save newFsm =
fsm <- newFsm
{
new Repository<'FSM> with
member this.read() = read()
member this.save(newFsm) = save newFsm
}
We can read the "state" of the policy after all interactions with the door:
// executed in FSI
knocksPolicyRepository.read()
val it: ThreeKnocksNotificationPolicy.SendingSmsOnThreeKnocksPolicy =
SendingSmsOnThreeKnocksPolicy
(FSM (KnocksCount 4, <fun:sendingSmsOnThreeKnocksPolicy@178-16>))
Phew, that was longer than expected, huh.
We explored stateless and stateful policies, and typically when thinking about the "state", one could think of "read-models".
Isn't it an excellent moment to make a transition (pun intended) to projections?
Third function - "a projection"
So according to the diagram, it is driven by events and it produces "read-models".
type Projection<'Event, 'ReadModel> =
FSM<'Event, 'ReadModel, 'ReadModel>
Hmm, but wait a second.
As we are discussing functions, and both "an aggregate" and "a policy" are functions with their corresponding blocks ("stickies") in the diagram, shouldn't "a projection" be a subject of the same property?
Meaning, shouldn't a function have a corresponding "sticky" in the diagram?
It seem that an arrow from a Domain Event to a Read Model is a bit of simplification and "transforming" effect is hidden "in the arrow".
Let's make "a projection" function explicit!
It feels somewhat right - "a projection" is expressed visually in the diagram.
In the original article, the author used Opened
event to be applied to a projection - we're going to use Unlocked
instead.
This might be a nice event to follow up with second-order events in the next tales.
We have all in place to represent "a projection" in code:
module Projections =
module UnlockedCounts =
type UnlockCount = UnlockCount of count: int
type UnlockCountProjection =
UnlockCountProjection of Projection<DoorEvent, UnlockCount>
type EvolveUnlockCountProjection =
Evolve<DoorEvent, UnlockCount, UnlockCountProjection>
It means that we are going to count how many times the door was unlocked.
let applyTo readModel event =
// UnlockCount -> DoorEvent -> UnlockCount
let (UnlockCount count) = readModel
let newCount =
match event, readModel with // 👈 we're deciding how to build "read model" with a new event
| Unlocked, UnlockCount count -> UnlockCount (count + 1)
| _ -> readModel
newCount
let evolve: EvolveUnlockCountProjection =
// DoorEvent -> UnlockCountProjection -> UnlockCount * UnlockCountProjection
fun event projection ->
let (UnlockCountProjection(FSM(readModel, applyTo))) = projection
let newCount = event |> applyTo readModel
newCount, UnlockCountProjection(FSM(newCount, applyTo))
let unlockCountProjection() = UnlockCountProjection(FSM(UnlockCount 0, applyTo))
Now we can subscribe to the events and react to them:
let unlockCountProjectionRepository =
InMemory.repositoryFor(Door.Projections.UnlockedCounts.unlockCountProjection)
subscribeToDoorEvents (fun event ->
let existingUnlocksCountProjection = unlockCountProjectionRepository.read()
let (UnlockCount count), newUnlocksCountProjection =
existingUnlocksCountProjection
|> Door.Projections.UnlockedCounts.evolve event
printfn $"Door unlocked {count} number of times"
unlockCountProjectionRepository.save newUnlocksCountProjection
)
Applying the same door interactions sequence as before, we're getting:
// as a reminder, sequence of door interactions we ran:
// close, knock, knock, lock, knocked, knocked, unlock
Door unlocked 0 number of times
Door unlocked 0 number of times
Door unlocked 0 number of times
Door unlocked 0 number of times
Door unlocked 0 number of times
Door unlocked 0 number of times
Door unlocked 1 number of times
Preditable as we unlocked the door only at the end of the sequence.
Same with reading the "state" of the projection using FSI:
unlockCountProjectionRepository.read()
val it: UnlockCountProjection =
UnlockCountProjection
(FSM (UnlockCount 1, <fun:unlockCountProjection@118-19>))
It looks strangely similar to the policy state, isn't it?
Maybe we should look at signatures of all applyTo
functions from modules (DoorState
, ThreeKnocksNotificationPolicyState
and Projections.UnlockedCounts
) related to "state"?
Door.DoorState
module
DoorState -> DoorEvent -> DoorState
ThreeKnocksNotificationPolicy.ThreeKnocksNotificationPolicyState
module
KnocksCount -> DoorEvent -> KnocksCount
Projections.UnlockedCounts
module
UnlockCount -> DoorEvent -> UnlockCount
All of them follow the same pattern - 'State -> 'Event -> 'State
- and it turns out that's exactly the function signature of "a projection"!
This means that both "a policy" and "an aggregate" functions used "a projection" function to "build" (or evolve) a "read-model" (or rather a state), to decide how to deal with incoming input, based on the current state.
Dear Reader, if you are interested in detailed origins of evolve and decide concepts, please depart to a great work by Jeremie Chassaing, titled Functional Event Sourcing Decider, and read it carefully.
It is very insightful and thought provoking!
Functions (and facts) described the world
What a journey!
We were able to visit three concepts from "tactical Domain-Driven Design" and Event Storming - "an aggregate", "a policy" and "a projection".
Each of them was modelled using a "state machine" - a basic structure to represent its "state" (which eventually for "a policy" and "an aggregate" turned out to be "a projection"'s state!) and "deciding function".
Thanks to F# and FSI, we were able to quickly get feedback from our findings.
Even though each of those concepts have three distinct names - all of them can be represented by functions, either driven by events or producing them.
"functions are just functions" - this is how the discussion ended up (at least the one I referred to) - which undeniably is true and I agree with it wholeheartedly.
I hope you were able to see it too, dear Reader.
At the same time, "strategic Domain-Driven Design" teaches us to organize the knowledge of the domain, in the form of the language, so that we are able to communicate clearly and effectively.
I believe this applies to those building blocks too - "an aggregate", "a policy" and "a projection" - as they are just functions, but they are also "a language" to communicate the intention behind.
Objects, functions and words
And it is not the fault of the functions to get misinterpreted - especially when it comes to our sweet "aggregates" - which often get bloated, heavy and full of unnecessary responsibilities.
"The beauty is in the eye of the beholder" - if someone wants to see "an object" in "an aggregate", soon "a state" might cover the whole thing.
Similar to a Zen Koan, we could express:
“When a wise man points at a set of rules to enforce consistent state change with a state, the fools look at the state, not at a set of rules to enforce consistent state change.”
Of course, one could argue "Damian, but each of FSMs you presented aggregated a state and a function! We saw objects, didn't we?!".
Even though there was "a state" involved, typically it is a private business of "an object" - its "implementation detail".
And yet again this "state" easily distracts us from the main capability of "an object" - to be able to collaborate with others via message-passing and transforming the incoming input into the output. (if you are interested in more, please check The ambiguity of objects)
We used the best language in the universe (no, I am not biased) - my beloved F# - to explore the transfoming nature of all those DDD building blocks.
It was easy to focus on that processing aspect, as functions are the first-class citizens in F# - which was followed by an immutable-first approach for modeling structures (even though if it wasn't emphasized earlier), giving birth to "events".
Functions are just functions and giving them names should nourish and ease the collaboration.
We reached the end of our journey, dear Reader - thank you for your patience and time, as we needed plenty of both.
Til the next time!