A note of the Functional and Algebraic Domain Modeling talk given by Debasish Ghosh.

FP, Algebra and Domain Modeling

Fis programming with pure function. output purely determined by the input. Pure mapping between values. No assignment, no side-effects Functions compose. Expression-oriented programming.

John Carmack: sometimes, the elegant implementation is just a function. Not a method, not a class , not a framework. Functions are mathematics, and maths are elegant, hardly anything can be more elegant than math.

Algebra: is the study of algebraic structures. An algebraic structure has three parts:

  • A set (carrier set or underlying set) with
  • one or more finitely operations defined on the set
  • satisfies a list of axioms

Algebraic thinking is reasoning about code in terms of data types and the operations they support without considering the underlying operation implementations.

For function f : A => B and g: B => C, wen can compose them as h: A => C only when they are our functions that don’t throw exception or perform any side effects.

Domain is described in a set of bounded contexts. A bounded context has a consistent vocabulary, a set of domain behaviors modeled as functions on domain objects implemented as types, each of the behaviors honor a set of business rules, related behaviors grouped as modules. Modules have algebraic structures.

Functions work on types (domain objects).

Domain model = union of bounded context (i). A bounded context has a set of modules. A module has a set of types and a set of functions and business rules.

Domain functions can be composed and are closed under composition.

Functions are morphisms, types are sets. There are composition and rules/laws.

Domain model algebra: algebra of types, functions and laws of the solution domain model. Explicit: types, type constraints, functions between types. Verifiable: Type Constraints, more with DT, algebraic property based testing.

Algebra of Types and Modules

Sum types: boolean, enumeration, option, either, failure(try).
Using sealed trait and case class/objects. Then pattern match with exhaustive checking.

More algebra of types: exponential f: A => B has b ** a inhabitants, Taylor series: recursive data types Derivatives: zippers

A function is a mapping from the domain of types to the co-domain of types, that’s an algebra of a function.

A module is a collection of related functions, that’s an algebra of a module.

A domain model is a collection of modules, that is a domain model algebra.

Generic: parametric data types. Lis is a type constructor . Clear separation between the contract (the algebra) and its implementation (interpreters). Standard vocabulary (like pattern). Existing set of reusable algebras offered by libraries.

Example:

  1. identify domain behaviors
  2. identify the algebra of functions (not implementation)
  3. compose algebras to form large behaviors (a program)
  4. Plug in concrete types to complete the implementation.

An Example

Issues: composition and side effects (violates modularity). Use algebra, type constructor, as a rescue to fix side effects.

1
2
3
4
5
6
7
trait Trading {
  def fromClientOrder: ClientOrder => Order
  def execute(market, Market, brokerAccount: Account): Order => List[Execution]
  def allocate(accounts: List[Account]): List[Execution] => List[Trade]
}

trait TradeComponent extends Trading with Logging with Auditing

The effect type F[_] make the module an effectful module and the functions effectful functions. In F[A], F is the side effect, A is the type of result. F[_] is an opaque type that handles the error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait Trading[F[_]] {
  def fromClientOrder: ClientOrder => F[Order]
  def execute(market, Market, brokerAccount: Account): Order => F[List[Execution]]
  def allocate(accounts: List[Account]): List[Execution] => F[List[Trade]]
}

// trading is the interpreter/implementation that generates the effect F[_]
// we want to run them sequentially: behavior composition
def tradeGenerationProgram[M[_]: Monad](trading: Trading[M]) = for {
  order <- trading.fromClientOrder(clientOrder)
  executions <- trading.execute(market, brokerAccount, order)
  trades <- trading.allocate(List(account1, account2, account3), executions)
} yield trades

// with Logging
def tradeGenerationWithLogPrgram[M[_]: Monad](
  trading: Trading[M], logger: Logger[M]) = for {
  _ <- logger.info("start processing")
  order <- trading.fromClientOrder(clientOrder)
  executions <- trading.execute(market, brokerAccount, order)
  trades <- trading.allocate(List(account1, account2, account3), executions)
  _ <- logger.info("allocatioin done")
} yield trades

An interpreter is an implementaiton of the domain model. It handles the error path of execution and the actual effect calls. A sample interpreter could be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class TradingInterpreter[F[_]] (implict me: MonadError[F, Throwable]) extends Trading[F] {
  def fromClientOrder: ClientOrder => F[Order] = makeOrder(_) match {
    case Left(error) => me.rasieError(new Exception(error.mesage))
    case Right(order) => order.pure[F]
  }

  def execute(market: Market, brokerAccount: Account): Order => F[List[Execution]] = ???
  def allocate(accounts: List[Account]): List[Execution] => F[List[Trade]] = ???
}

class LoggerIntepreter[F[_]] extends Logger[F] {
  def info(message: String) = ???
}

A big benefit of module is composition.

Finally, the runtime. [IO] or [Task] deal with the effects such as input/output and execution.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import cats.effect.IO
object TradingComponent extends TradingInterpreter[IO]
tradeGenerationProgram(TradingComponent).unsafeRunSync

// run with logging
object LoggerComponent extends LoggerIntepreter[IO]
tradeGenerationWithLogPrgram(TradingComponent, LoggerComponent).unsafeRunSync

// or use a different runtime
import monix.eval.Task
object TradingComponent extends TradingInterpreter[Task]
tradeGenerationProgram(TradingComponent)

Effects and side-effects are not the same thing. Effects are algebraic and are good, side-effects are bugs.

Takeaways

  • Algebra scales from a single data type to an entire bounded context.
  • Algebras compose enabling composition of domain behaviors.
  • Algebras let you focus on the compositionality without any context of implementation.
  • Statically typed functional programming is programming with algebras.
  • Abstract early, interprete as late as possible.
  • Abstractions/functions compose only when they are abstract and parametric.
  • Modularity in the presence of side-effects is a challenge.
  • Effects as algebras are pure values that can compose based on laws.
  • Honor the law of using the least powerful abstraction that works.