The main theme of the talk is how to use algebraic and functional techniques to build modular domain models that are pure and compositional even in the presence of side-effects. I discuss the use of pure algebraic effects to abstract side-effects thereby keeping the model compositional.
5. — John Hughes in Why Functional Programming Matters
https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf
“[..] modularity is the key to successful programming. Languages
that aim to improve productivity must support modular programming well.
But new scope rules and mechanisms for separate compilation are not
enough — modularity means more than modules. Our ability to
decompose a problem into parts depends directly on our ability to glue
solutions together.To support modular programming, a language must
provide good glue. [..] Using these glues one can modularize programs in
new and useful ways[…]. Smaller and more general modules can be reused
more widely, easing subsequent programming.This explains why functional
programs are so much smaller and easier to write than conventional
ones.”
6. Intent
• Domain Model
• Domain Model Algebra
• Algebraic Combinators
• Compositionality
• Algebra as the basis of modularization of domain models
• Algebraic effects to keep your domain model pure,
modular and compositional
14. What is a domain model ?
A domain model in problem solving and software engineering is a
conceptual model of all the topics related to a specific problem. It
describes the various entities, their attributes, roles, and
relationships, plus the constraints that govern the problem domain.
It does not describe the solutions to the problem.
Wikipedia (http://en.wikipedia.org/wiki/Domain_model)
27. 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
28. Domain Model = ∪(i) Bounded Context(i)
Bounded Context = { m[T1,T2,..] | T(i) ∈ Types }
Module = { f(x,y,..) | p(x,y) ∈ Domain Rules }
• domain function
• on an object of types x, y, ..
• composes with other functions
• closed under composition
• business rules
30. Domain Model Algebra
explicit verifiable
• types
• type constraints
• functions between types
• type constraints
• more constraints if you have DT
• algebraic property based testing
(algebra of types, functions & laws
of the solution domain model)
31. Domain Model Algebra
• Algebra as the glue for binding domain model
artifacts
• Algebras evolve by composition
Reusable combinators
Build larger abstractions out
of smaller ones using properties
of compositionality
32. Algebra of a Monoid
trait Semigroup[A] {
def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
parametricity
33. Algebra of a Foldable
trait Foldable[F[_]] {
def foldleft[A,B](as: F[A], z: B, f: (B, A) => B): B
def foldMap[A,B](as: F[A], f: A => B)
(implicit m: Monoid[B]): B =
foldleft(as, m.zero,
(b: B, a: A) => m.combine(b, f(a)))
}
34. Algebraic Combinators
def mapReduce[F[_], A, B](as: F[A])(f: A => B)
(implicit fd: Foldable[F], m: Monoid[B]): B =
fd.foldMap(as)(f)
Built out of PURE algebra ONLY
Uses the algebras of Monoid and Foldable
36. Algebras => Functional Patterns
• Bits of domain elements evolving from reusable
generic algebras
• The algebras we reuse already exist - as a designer
we provide the implementation of those
algebras in the context of the domain model
• The algebras are the patterns, the
implementations are instances of patterns in the
context of our domain model
37. Domain Model = ∪(i) Bounded Context(i)
Bounded Context = { m[T1,T2,..] | T(i) ∈ Types }
Module = { f(x,y,..) | p(x,y) ∈ Domain Rules }
• domain function
• on an object of types x, y
• composes with other functions
• closed under composition
• business rules
38. • domain function
• on an object of type x, y, ..
• composes with other functions
• closed under composition
Domain Model = ∪(i) Bounded Context(i)
Bounded Context = { m[T1,T2,..] | T(i) ∈ Types }
Module = { f(x, y, .. ) | p(x) ∈ Domain Rules }
• business rules
(algebra)
(algebra)
39. Given all the properties of algebra, can we
consider algebraic composition to be the
basis of designing, implementing and modularizing
domain models ?
50. Client places order
- flexible format
Transform to internal domain
model entity and place for execution
1 2
51. Client places order
- flexible format
Transform to internal domain
model entity and place for execution
Trade & Allocate to
client accounts
1 2
3
52. def fromClientOrder: ClientOrder => Order
def execute(market: Market, brokerAccount: Account)
: Order => List[Execution]
def allocate(accounts: List[Account])
: List[Execution] => List[Trade]
trait Trading {
}
trait TradeComponent extends Trading
with Logging with Auditing
algebra of domain
behaviors / functions
functions aggregate
upwards into modules
modules aggregate
into larger modules
53. Takeaways ..
• Publish as much domain behavior as you can through the
algebra of your APIs. Since algebra is compositional, it’s easy
to extend later by stacking abstractions on top of existing ones.
• Types are important but in domain modeling, names must come
from the vocabulary of the domain - Ubiquitous Language
• Group related functions into modules. Modules are also
compositional in Scala.
• Functions aggregate into modules, modules aggregate into
components.
54. .. so we have a decent algebra of our module, the
names reflect the appropriate artifacts from the
domain (ubiquitous language), the types are
well published and we are quite explicit in what
the behaviors do ..
55. 1. Compositionality - How do we compose
the 3 behaviors that we published to
generate trade in the market and allocate
to client accounts ?
2. Side-effects - We need to compose them
alongside all side-effects that form a core
part of all non trivial domain model
implementations
56. • Error handling ?
• throw / catch exceptions is not RT
• Partiality ?
• partial functions can report runtime exceptions if invoked
with unhandled arguments (violates RT)
• Reading configuration information from environment ?
• may result in code repetition if not properly handled
• Logging ?
• side-effects
Side-effects
57. Side-effects
• Database writes
• Writing to a message queue
• Reading from stdin / files
• Interacting with any external resource
• Changing state in place
59. .. the semantics of compositionality ..
in the presence of side-effects
60. The solution is to abstract side-effects into data
types that are pure values for which
referential transparency holds and which can
be composed with other pure functional
abstractions
61. The solution is to abstract side-effects into
data types that are pure values for which
referential transparency holds and which can be
composed with other pure functional
abstractions
62. The solution is to abstract side-effects into
data type-constructors that are pure values
for which referential transparency holds and which
can be composed with other pure
functional abstractions
Effects
64. F[A]
The answer that the
effect computesThe additional stuff
modeling the computation
65. • The F[_] that we saw is an opaque type - it
has no denotation till we give it one
• The denotation that we give to F[_] depends
on the semantics of compositionality that we
would like to have for our domain model
behaviors
66. 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]]
trait Trading {
}
• We haven’t yet given any denotation to the effect type
• We haven’t yet committed to any concrete effect type
67. • .. we have intentionally kept the algebra open
for interpretation ..
• .. there are use cases where you would like to
have multiple interpreters for the same
algebra ..
68. class TradingInterpreter[F[_]]
(implicit me: MonadError[F, Throwable])
extends Trading {
def fromClientOrder: ClientOrder => F[Order] = makeOrder(_) match {
case Left(dv) => me.raiseError(new Exception(dv.message))
case Right(o) => o.pure[F]
}
def execute(market: Market, brokerAccount: Account)
: Order => F[List[Execution]] = ...
def allocate(accounts: List[Account])
: List[Execution] => F[List[Trade]] = ...
}
One Sample Interpreter
69. • .. one lesson in modularity - commit to a
concrete implementation as late as
possible in the design ..
• .. we have just indicated that we want a
monadic effect - we haven’t committed to
any concrete monad type even in the
interpreter ..
70. The Program
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor)
executions <- T.execute(m1, ba, order)
trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
71. The Program
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor)
executions <- T.execute(m1, ba, order)
trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
depends on the
algebra only
72. The Program
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor)
executions <- T.execute(m1, ba, order)
trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
depends on the
algebra only
the story of what needs
to be done, NOT HOW
73. The Program
import cats.effect.IO
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor)
executions <- T.execute(m1, ba, order)
trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
object TradingComponent extends TradingInterpreter[IO]
tradeGeneration(TradingComponent).unsafeRunSync
74. The Program
import monix.eval.Task
def tradeGeneration[M[_]: Monad](T: Trading[M]) = for {
order <- T.fromClientOrder(cor)
executions <- T.execute(m1, ba, order)
trades <- T.allocate(List(ca1, ca2, ca3), executions)
} yield trades
object TradingComponent extends TradingInterpreter[Task]
tradeGeneration(TradingComponent)
75. The Program
def tradeGenerationLoggable[M[_]: Monad]
(T: Trading[M], L: Logging[M]) = for {
_ <- L.info("starting order processing")
order <- T.fromClientOrder(cor)
executions <- T.execute(m1, ba, order)
trades <- T.allocate(List(ca1, ca2, ca3), executions)
_ <- L.info("allocation done")
} yield trades
object TradingComponent extends TradingInterpreter[IO]
object LoggingComponent extends LoggingInterpreter[IO]
tradeGenerationLoggable(TradingComponent, LoggingComponent).unsafeRunSync
79. - Rob Norris at scale.bythebay.io talk - 2017 (https://www.youtube.com/
watch?v=po3wmq4S15A)
“Effects and side-effects are not the same thing. Effects are
good, side-effects are bugs.Their lexical similarity is really
unfortunate because people often conflate the two ideas”
80. Testing
• Testable
• We did not commit to a
concrete type upfront - some
virtues of being lazy with
evaluation
• For monadic effects, easier
testing with the Id monad -
just use a different
implementation for the same
algebra
def tradeGenerationLoggable[M[_]: Monad]
(T: Trading[M], L: Logging[M]) = for {
_ <- L.info("starting order processing")
order <- T.fromClientOrder(cor)
executions <- T.execute(m1, ba, order)
trades <- T.allocate(List(ca1, ca2, ca3), executions)
_ <- L.info("allocation done")
} yield trades
81.
82.
83.
84. Takeaways
• Modularity in the presence of side-effects is a
challenge
• Algebraic modeling is the key to address this
• Effects as algebras are pure values that can
compose based on laws
• Determine the type of effect based on the
semantics of compositionality of your domain
behaviors
85. Takeaways
• Compose effects parametrically
• Honor the law of using the least powerful
abstraction that works
• Be polymorphic (parametric) as early as you
can, commit to concrete types as late as you
can