home..

Types And Functions

Programming starts with types and functions.

Algebraic Data Types

In computer programming, especially functional programming, and type theory, an algebraic data type is a kind of composite / a type formed by combining other types.

Two standard classes of algebraic types are:

They provide the necessary abstraction for structuring the various data of our domain model.

Sum type models an OR relationship, and Product type models an AND relationship. So, the OR and AND operations constitute the algebra of our data types.

Structuring the various data of our domain model

In a previous blog post, you learned that any information system is responsible for handling the intent (Command) and, based on the current State, produce new facts/decisions (Events). The system’s new State is then evolved out of these Events .

event modeling

It is becoming clear that we have various classes of algebraic data types in our domain. Let’s categorize them:

We need statically typed, multi-paradigm programming language to exercise domain modeling, with functions as first-class citizens included. To name some: Haskell, Kotlin, Scala, F#, Rust, TypeScript. For this blog post, we are going to focus on Kotlin, showing you the power of a hybrid functional, object-oriented approach.

Commands

Commands represent the intent to change the state of the information system.

Sum/OR

The sealed class in Kotlin represents data composition, also known as a Sum type (models the OR relationship).

The key benefit of using sealed classes comes into play when using them in a when expression. If it’s possible to verify that the statement covers all cases, you don’t need to add an else clause to the statement.

We model our commands as a Sum type (OR relationship) by using the sealed class. In this example, we have two possible sub-classes of RestaurantOrderCommand which are known at compile time: CreateRestaurantOrderCommand and MarkRestaurantOrderAsPreparedCommand.

sealed class Command

sealed class RestaurantOrderCommand : Command() {
   abstract val identifier: RestaurantOrderId
}

data class CreateRestaurantOrderCommand(
   override val identifier: RestaurantOrderId,
   val restaurantIdentifier: RestaurantId = RestaurantId(),
   val lineItems: ImmutableList<RestaurantOrderLineItem>
) : RestaurantOrderCommand()

data class MarkRestaurantOrderAsPreparedCommand(
   override val identifier: RestaurantOrderId
) : RestaurantOrderCommand()

Other programming languages are using different mechanisms to model OR/Sum relationship. For example, TypeScript has Unions:

type RestaurantOrderCommand = CreateRestaurantOrderCommand | MarkRestaurantOrderAsPreparedCommand;

Product/AND

If you zoom in into the concrete command types, for example, CreateRestaurantOrderCommand, you will notice that it is formed by combining other types: RestaurantOrderId, RestaurantId, RestaurantOrderLineItem. Essentially, CreateRestaurantOrderCommand data class is a Product type which models AND relationship:

CreateRestaurantOrderCommand = RestaurantOrderId & RestaurantId & list of [RestaurantOrderLineItem]

Events

Events represent the state change itself, a fact. These events represent decisions that have already happened (past tense).

Sum/OR

We model our events as a Sum type (OR relationship) by using sealed class. In this example, we have four possible sub-classes of RestaurantOrderEvent which are known at compile time: RestaurantOrderCreatedEvent , RestaurantOrderPreparedEvent, RestaurantOrderRejectedEvent and RestaurantOrderNotPreparedEvent.

sealed class Event

sealed class RestaurantOrderEvent : Event() {
   abstract val identifier: RestaurantOrderId
}

sealed class RestaurantOrderErrorEvent : RestaurantOrderEvent() {
  abstract val reason: String
}

data class RestaurantOrderCreatedEvent(
   override val identifier: RestaurantOrderId,
   val lineItems: ImmutableList<RestaurantOrderLineItem>,
   val restaurantId: RestaurantId
) : RestaurantOrderEvent()

data class RestaurantOrderPreparedEvent(
   override val identifier: RestaurantOrderId,
) : RestaurantOrderEvent()

data class RestaurantOrderRejectedEvent(
   override val identifier: RestaurantOrderId,
   override val reason: String
) : RestaurantOrderErrorEvent()

data class RestaurantOrderNotPreparedEvent(
   override val identifier: RestaurantOrderId,
   override val reason: String
) : RestaurantOrderErrorEvent()

Product/AND

If you zoom in into the concrete event types, for example, RestaurantOrderCreatedEvent, you will notice that it is formed by combining other types: RestaurantOrderId, RestaurantId, RestaurantOrderLineItem. Essentially, RestaurantOrderCreatedEvent data class is a Product type which models AND relationship:

RestaurantOrderCreatedEvent = RestaurantOrderId & RestaurantId & list of [RestaurantOrderLineItem]

State

The current state of the information system is evolved out of past events/facts.

data class RestaurantOrder(
   val id: RestaurantOrderId,
   val restaurantId: RestaurantId,
   val status: Status,
   val lineItems: ImmutableList<RestaurantOrderLineItem>
)

Product/AND

If you zoom in into the concrete state types, for example, RestaurantOrder, you will notice that it is formed by combining other types: RestaurantOrderId, RestaurantId, RestaurantOrderLineItem, Status. Essentially, RestaurantOrder data class is a Product type that models AND relationship:

RestaurantOrder = RestaurantOrderId & RestaurantId & Status & list of [RestaurantOrderLineItem]

Embrace Immutability

Kotlin encourages developers to write immutably, by using val in your data types. Immutable objects are thread safe. No race conditions, no concurrency problems, no need to sync.

We can afford it!

Encapsulation

One might object that algebraic data types violate encapsulation by making public the internal representation of a type. In functional programming, we approach concerns about encapsulation differently / we don’t typically have a delicate mutable state which could lead to bugs or violation of invariants if exposed publicly. Exposing the data constructors of a data type is often fine, and the decision to do so is approached much like any other decision about what the public API of a data type should be.

Book - Functional Programming in Kotlin

In order to achieve better encapsulation, one could use Interfaces instead of Data classes as a public API by restricting Data classes to package private. Taking into account that we achieved a great deal of immutability, this might not be needed.

Modeling the Behaviour of our domain

This leads to modularity in design and a clear separation of the entity’s structure and functions/behaviour of the entity.

Fmodel library1 offers generic and abstract components to specialize in for your specific case/expected behavior:

Decider

The decider is a data type that represents the main decision-making algorithm.

decider image

fun restaurantOrderDecider() = Decider<RestaurantOrderCommand?, RestaurantOrder?, RestaurantOrderEvent?>(
   // Initial state of the Restaurant Order is `null`. It does not exist.
   initialState = null,
   // Exhaustive command handler(s): for each type of [RestaurantCommand] you are going to publish specific events/facts, as required by the current state/s of the [RestaurantOrder].
   decide = { c, s ->
       when (c) {
           is CreateRestaurantOrderCommand ->
               // ** positive flow **
               if (s == null) flowOf(RestaurantOrderCreatedEvent(c.identifier, c.lineItems, c.restaurantIdentifier))
               // ** negative flow - option 1 (publishing business error events) **
               else flowOf(RestaurantOrderRejectedEvent(c.identifier, "Restaurant order already exists"))
               // ** negative flow - option 2 (publishing empty flow of events - ignoring negative flows - we are losing information :( ) **
           //  else emptyFlow()
               // ** negative flow - option 3 (throwing exception - we are losing information - filtering exceptions is fragile) **
           //  else flow { throw RuntimeException("Restaurant order already exists") }
           is MarkRestaurantOrderAsPreparedCommand ->
               // ** positive flow **
               if ((s != null && CREATED == s.status)) flowOf(RestaurantOrderPreparedEvent(c.identifier))
               // ** negative flow **
               else flowOf(RestaurantOrderNotPreparedEvent(c.identifier, "Restaurant order does not exist / not in CREATED status"))
           null -> emptyFlow() // We ignore the `null` command by emitting the empty flow. Only the Decider that can handle the `null` command can be combined (Monoid) with other Deciders.
       }
   },
   // Exhaustive event handler(s): for each event of type [RestaurantEvent] you are going to evolve from the current state/s of the [RestaurantOrder] to a new state of the [RestaurantOrder]
   evolve = { s, e ->
       when (e) {
           is RestaurantOrderCreatedEvent -> RestaurantOrder(e.identifier, e.restaurantId, CREATED, e.lineItems)
           is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED)
           is RestaurantOrderErrorEvent -> s // Error events are not changing the state / We return the current state instead.
           null -> s // Null events are not changing the state / We return the current state instead. Only the Decider that can handle `null` event can be combined (Monoid) with other Deciders.
       }
   }
)

View

The view is a data type that represents the event handling algorithm responsible for translating the events into the denormalized state, which is adequate for querying.

view image

fun restaurantOrderView() = View<RestaurantOrderView?, RestaurantOrderEvent?>(
   // Initial state of the [RestaurantOrderView] is `null`. It does not exist.
   initialState = null,
   // Exhaustive event handler(s): for each event of type [RestaurantOrderEvent] you are going to evolve from the current state/s of the [RestaurantOrderView] to a new state of the [RestaurantOrderView].
   evolve = { s, e ->
       when (e) {
           is RestaurantOrderCreatedEvent -> RestaurantOrderView(e.identifier, e.restaurantId, CREATED, e.lineItems)
           is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED)
           is RestaurantOrderErrorEvent -> s // We ignore the `error` event by returning current State/s.
           null -> s // We ignore the `null` event by returning current State/s. Only the View that can handle `null` event can be combined (Monoid) with other Views.
       }
   }
)

Saga

Saga is a data type that represents the central point of control, deciding what to execute next. It is responsible for mapping different events from deciders into action results that the Saga then can use to calculate the subsequent actions to be mapped to the command of other deciders.

In the context of smart endpoints and dumb pipes, deciders would be smart endpoints, and saga would be a dumb pipe.

saga image

fun restaurantOrderSaga() = Saga<RestaurantEvent?, RestaurantOrderCommand?>(
   react = { e ->
       when (e) {
           is RestaurantOrderPlacedAtRestaurantEvent -> flowOf(
               CreateRestaurantOrderCommand(
                   e.restaurantOrderId,
                   e.identifier,
                   e.lineItems
               )
           )
           is RestaurantCreatedEvent -> emptyFlow()
           is RestaurantMenuActivatedEvent -> emptyFlow()
           is RestaurantMenuChangedEvent -> emptyFlow()
           is RestaurantMenuPassivatedEvent -> emptyFlow()
           is RestaurantErrorEvent -> emptyFlow()
           null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas.
       }
   }
)

Totality

A function is total if it is defined for all of its possible inputs.

By having algebraic data types modeling the Sum/OR relationship with sealed class, it’s possible to verify that the when expression covers all cases, you don’t need to add an else clause to the statement. This is known as Kotlin matching. Many modern languages have support for some kind of pattern matching.

The compiler will yell at you if you add a new command or event into the model/project (when expression goes red), and you will have to fix it immediately. It will positively influence the function (decide, evolve, react) totality2 giving more guarantees about code correctness.

The essence of functional programming lies in the power of pure functions. Add static types to the mix, and you have algebraic abstractions—functions operating on types and honoring certain laws. Make the functions generic on types, and you have parametricity. The function becomes polymorphic, which implies more reusability, and if you’re disciplined enough not to leak any implementation details by sneaking in specialized types (or unmanaged hazards such as exceptions), you get free theorems.

In the following blog posts, we are going to zoom out and observe how this design fits the onion architecture / ports and adapters.

onion architecture image

The source code of the demo application is publicly available here

Happy coding!


  1. FModel can be used as a library, or as an inspiration, or both. It provides just enough tactical Domain-Driven Design patterns, optimised for Event Sourcing and CQRS. 

  2. This blog post provides a comprehensive guide to the concept of totality in Functional Programming by demystifying its meaning, giving a lot of examples, and recommending how to get tools to help you write maintainable, testable code. 

© 2024 fraktalio d.o.o.