This is going to be a discussion about design patterns. But I promise it’s going to be very different from the Gang of Four patterns that we all have used and loved in Java.
It doesn’t have any mathematics or category theory - it’s about developing an insight that lets u identify code structures that u think may be improved with a beautiful transformation of an algebraic pattern.
In earlier days of Java coding we used to feel proud when we could locate a piece of code that could be transformed into an abstract factory and the factory bean could be injected using Spring DI. The result was we ended up maintaining not only Java code, but quite a bit of XML too, untyped and unsafe. This was the DI pattern in full glory. In this session we will discuss patterns that don’t look like external artifacts, they are part of the language, they have some mathematical foundations in the sense that they have an algebra that actually compose and compose organically to evolve larger abstractions.
8. Solution to a Problem in Context
Design Pattern
we are given a problemgeneric component
(invariant across
context of application)
context dependent
(varies with the
context of problem)
9. What is an Algebra ?
Algebra is the study of algebraic structures
In mathematics, and more specifically in abstract algebra,
an algebraic structure is a set (called carrier
set or underlying set) with one or more finitary
operations defined on it that satisfies a list of axioms
- Wikipedia
(https://en.wikipedia.org/wiki/Algebraic_structure)
10. Set A
ϕ : A × A → A
for (a, b) ∈ A
ϕ(a, b)
a ϕ b
given
a binary operation
for specific a, b
or
The Algebra of Sets
11. 3 + 2 = 5
7 + 4 = 11
2 + 0 = 2
0 + 6 = 6
8 + 9 = 9 + 8.
Binary operation
Identity operation
Associative operation
always produces an integer
(closure of operations)
One specific instance of the Algebra
12. Set A
ϕ : A × A → A
given
a binary operation
(a ϕ b) ϕ c = a ϕ (b ϕ c)
associative
for (a, b, c) ∈ A
Let’s enhance the Algebra ..
13. Set A
ϕ : A × A → A
given
a binary operation
(a ϕ b) ϕ c = a ϕ (b ϕ c)
associative
for (a, b, c) ∈ A
The Algebra of Semigroups
14. Set A
ϕ : A × A → A
given
a binary operation
(a ϕ b) ϕ c = a ϕ (b ϕ c)
associative
for (a, b, c) ∈ A
a ϕ I = I ϕ a = a
for (a, I ) ∈ A
identity
The Algebra of Monoids
16. Algebra <=> Protocol with Laws
class Monoid a where
mempty :: a
mappend :: a -> a -> a
-- Identity laws
x <> mempty = x
mempty <> x = x
-- Associativity
(x <> y) <> z = x <> (y <> z)
17. Monoid In Scala
trait Semigroup[A] {
def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
18. Monoid In Scala
trait Semigroup[A] {
def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
val intAdditionMonoid: Monoid[Int] = new Monoid[Int] {
def empty: Int = 0
def combine(x: Int, y: Int): Int = x + y
}
• algebra (interface)
• reusable
• polymorphic
• standard library code
• instance (implementation)
• specific for a datatype
val moneyAdditionMonoid: Monoid[Money] = new Monoid[Money] {
def empty: Money = ???
def combine(x: Money, y: Money): Money = ???
}
• domain specific instance
• specific for Money
• application library code
19. Monoid In Scala
trait Semigroup[A] {
def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
val intAdditionMonoid: Monoid[Int] = new Monoid[Int] {
def empty: Int = 0
def combine(x: Int, y: Int): Int = x + y
}
• algebra (interface)
• reusable
• polymorphic
• standard library code
• instance (implementation)
• specific for a datatype
val moneyAdditionMonoid: Monoid[Money] = new Monoid[Money] {
def empty: Money = ???
def combine(x: Money, y: Money): Money = ???
}
• domain specific instance
• specific for Money
• application library code
Generic
Specific
• Specific implementations
use the generic protocol/interface
• This reusability is enforced by
parametricity (no type specific info
in the protocol)
• Genericity implies reusability
20. Monoid In Scala
trait Semigroup[A] {
def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
val intAdditionMonoid: Monoid[Int] = new Monoid[Int] {
def empty: Int = 0
def combine(x: Int, y: Int): Int = x + y
}
• algebra (interface)
• reusable
• polymorphic
• standard library code
• instance (implementation)
• specific for a datatype
val moneyAdditionMonoid: Monoid[Money] = new Monoid[Money] {
def empty: Money = ???
def combine(x: Money, y: Money): Money = ???
}
• domain specific instance
• specific for Money
• application library code
Pattern
Instances
generic & reusable
context specific
21. Functional Patterns
• Generic, reusable algebra
• Parametric on types
• Clear separation between pattern (algebra) and
its instances
• Composable through function composition
22. Functional Patterns
• Standard vocabulary - people know these terms,
know these operations and their types
• Rich ecosystem support through standard
libraries
• Functions defined in only terms of these
interfaces / algebra can be reused by application
level data types that follow the pattern
23. Functional Patterns - freebies
• Given a type that has an instance of a Monoid, if we have a
List of such objects, we can combine them for free using
the combine function of Monoid.Also note that the
behavior of combine is completely polymorphic - depends
on the instance of Monoid that you pass in.
• Given a typeV that has an instance of a Monoid, Map[K,
V] also gets a Monoid. Part of standard library, but as an
application developer you get this for free.
• .. and there are many such examples ..
24. Functional Patterns - Multiplicative Power
Money: Monoid
Payment: Monoid
Foo: Monoid
Bar: Monoid
Polymorphic behaviors in the
library that expects a Monoid
Domain Model Types
with Monoid instances
def fold[A](fa: F[A])
(implicit A: Monoid[A]): A = // ..
def foldMap[A, B](fa: F[A])(f: A => B)
(implicit B: Monoid[B]): B = // ..
def foldMapM[G[_], A, B](fa: F[A])
(f: A => G[B])(implicit G: Monad[G], B: Monoid[B]): G[B] = // ..
(multiply)
25. Domain Model
// a sum type for Currency
sealed trait Currency
case object USD extends Currency
case object AUD extends Currency
case object JPY extends Currency
case object INR extends Currency
// a Money can have denominations in multiple
// currencies
class Money (val items: Map[Currency, BigDecimal]) {
def toBaseCurrency: BigDecimal =
items.foldLeft(BigDecimal(0)) { case (a, (ccy, amount)) =>
a + Money.exchangeRateWithUSD.get(ccy).getOrElse(BigDecimal(1)) * amount
}
def isDebit = toBaseCurrency < 0
}
26. Domain Model
object Money {
final val zeroMoney =
new Money(Map.empty[Currency, BigDecimal])
// smart constructor
def apply(amount: BigDecimal, ccy: Currency) =
new Money(Map(ccy -> amount))
// concrete implementation: add two Money objects
def add(m: Money, n: Money) = new Money(
(m.items.toList ++ n.items.toList)
.groupBy(_._1)
.map { case (k, v) =>
(k, v.map(_._2).sum)
}
)
// sample implementation
final val exchangeRateWithUSD: Map[Currency, BigDecimal] =
Map(AUD -> 0.76, JPY -> 0.009, INR -> 0.016, USD -> 1.0)
}
concrete implementation
of fusing 2 Maps - can
we generalize it ?
27. Domain Model
import java.time.OffsetDateTime
// Account with a specific unique account no
case class Account(no: String, name: String, openDate: OffsetDateTime,
closeDate: Option[OffsetDateTime] = None) {
override def equals(o: Any): Boolean = o match {
case Account(`no`, _, _, _) => true
case _ => false
}
override def hashCode() = no. ##
}
// Payment made for a particular Account
case class Payment(account: Account, amount: Money,
dateOfPayment: OffsetDateTime)
28. Domain Model
import Money._
object Payments {
def creditAmount(p: Payment): Money =
if (p.amount.isDebit) zeroMoney else p.amount
// concrete implementation
def valuation(payments: List[Payment]): Money =
payments.foldLeft(zeroMoney) { (a, e) =>
add(a, creditAmount(e))
}
// concrete implementation that uses concrete methods of List
def maxPayment(payments: List[Payment]): Money =
payments.map(creditAmount).maxBy(_.toBaseCurrency)
// adjust balances and payments
def newBalances(currentBalances: Map[Account, Money],
currentPayments: Map[Account, Money]): Map[Account, Money] = {
// complicated logic that merges the 2 Maps
Map.empty[Account, Money]
}
}
complex implementation
of fusing 2 Maps - can
we generalize it ?
1. similar contract: Is there
any commonality of
behaviors that we can
extract ?
2. both iterate over the
collection and compute
an aggregate
29. Domain Model
object Money {
final val zeroMoney =
new Money(Map.empty[Currency, BigDecimal])
// smart constructor
def apply(amount: BigDecimal, ccy: Currency) =
new Money(Map(ccy -> amount))
// concrete implementation: add two Money objects
def add(m: Money, n: Money) = new Money(
(m.items.toList ++ n.items.toList)
.groupBy(_._1)
.map { case (k, v) =>
(k, v.map(_._2).sum)
}
)
// sample implementation
final val exchangeRateWithUSD: Map[Currency, BigDecimal] =
Map(AUD -> 0.76, JPY -> 0.009, INR -> 0.016, USD -> 1.0)
}
concrete implementation
of fusing 2 Maps - can
we generalize it ?
30. Domain Model
object Money {
final val zeroMoney =
new Money(Map.empty[Currency, BigDecimal])
// smart constructor
def apply(amount: BigDecimal, ccy: Currency) =
new Money(Map(ccy -> amount))
// concrete implementation: add two Money objects
def add(m: Money, n: Money) = new Money(
(m.items.toList ++ n.items.toList)
.groupBy(_._1)
.map { case (k, v) =>
(k, v.map(_._2).sum)
}
)
// sample implementation
final val exchangeRateWithUSD: Map[Currency, BigDecimal] =
Map(AUD -> 0.76, JPY -> 0.009, INR -> 0.016, USD -> 1.0)
}
(a) adds 2 Money objects
(b) has to be associative
identity operation
Pattern:
Monoid for Money!
31. Domain Model
import Money._
object Payments {
def creditAmount(p: Payment): Money =
if (p.amount.isDebit) zeroMoney else p.amount
// concrete implementation
def valuation(payments: List[Payment]): Money =
payments.foldLeft(zeroMoney) { (a, e) =>
add(a, creditAmount(e))
}
// concrete implementation that uses concrete methods of List
def maxPayment(payments: List[Payment]): Money =
payments.map(creditAmount).maxBy(_.toBaseCurrency)
// adjust balances and payments
def newBalances(currentBalances: Map[Account, Money],
currentPayments: Map[Account, Money]): Map[Account, Money] = {
// complicated logic that merges the 2 Maps
Map.empty[Account, Money]
}
}
complex implementation
of fusing 2 Maps - can
we generalize it ?
1. similar contract: Is there
any commonality of
behaviors that we can
extract ?
2. both iterate over the
collection and compute
an aggregate
32. Domain Model
import Money._
object Payments {
def creditAmount(p: Payment): Money =
if (p.amount.isDebit) zeroMoney else p.amount
// concrete implementation
def valuation(payments: List[Payment]): Money =
payments.foldLeft(zeroMoney) { (a, e) =>
add(a, creditAmount(e))
}
// concrete implementation that uses concrete methods of List
def maxPayment(payments: List[Payment]): Money =
payments.map(creditAmount).maxBy(_.toBaseCurrency)
// adjust balances and payments
def newBalances(currentBalances: Map[Account, Money],
currentPayments: Map[Account, Money]): Map[Account, Money] = {
// complicated logic that merges the 2 Maps
Map.empty[Account, Money]
}
}
2 different ways to combine a
bunch of Money instances
(a) combine to add
(b) combine to find the max
You get a monoid for Map[K, V]
if V has a monoid - part of standard
library.
(a) combine the 2 Maps
Pattern:
Monoid for Money!
33. Looking Back
• Similar problem
• Different context
• Reuse of the same algebra
• Different concrete instances of the algebra
34. import Money._
object Payments {
def creditAmount(p: Payment): Money =
if (p.amount.isDebit) zeroMoney else p.amount
// concrete implementation
def valuation(payments: List[Payment]): Money =
payments.foldLeft(zeroMoney) { (a, e) =>
add(a, creditAmount(e))
}
// concrete implementation that uses concrete methods of List
def maxPayment(payments: List[Payment]): Money =
payments.map(creditAmount).maxBy(_.toBaseCurrency)
// adjust balances and payments
def newBalances(currentBalances: Map[Account, Money],
currentPayments: Map[Account, Money]): Map[Account, Money] = {
// complicated logic that merges the 2 Maps
Map.empty[Account, Money]
}
}
Domain Model
• An aggregation that produces
a single result
• Do we really need a List for the
operation that we are doing ?
• Use the least powerful abstraction
that you need
Pattern: Foldable
35. Abstracting over Structure & Operation
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)))
}
def mapReduce[F[_], A, B](as: F[A], f: A => B)
(implicit ff: Foldable[F], m: Monoid[B]) =
ff.foldMap(as, f)
36. Domain Model
object Payments extends MoneyInstances with Utils {
def creditAmount: Payment => Money = { p =>
if (p.amount.isDebit) zeroMoney else p.amount
}
def valuation(payments: List[Payment]): Money = {
implicit val m: Monoid[Money] = MoneyAddMonoid
mapReduce(payments)(creditAmount)
}
def maxPayment(payments: List[Payment]): Money = {
implicit val m: Monoid[Money] = MoneyOrderMonoid
mapReduce(payments)(creditAmount)
}
def newBalances(currentBalances: Map[Account, Money],
currentPayments: Map[Account, Money]): Map[Account, Money] = {
implicit val m = MoneyAddMonoid
currentBalances |+| currentPayments
}
}
• Completely generic implementation
with Money manipulation logic
moved to the library code
• Reusability FTW
37. Parametricity
def mapReduce[F[_], A, B](as: F[A], f: A => B)
(implicit ff: Foldable[F], m: Monoid[B]) =
ff.foldMap(as, f)
• Parametric polymorphism
• No dependence on concrete types
• Reusable under multiple implementation context
• Limited implementation possibilities by definition
• Honors the principle of using the least powerful abstraction that works
• The most important virtue of good functional patterns
38. Domain Model (Handling Domain Validations)
private void validateState() throws ModelCtorException {
ModelCtorException ex = new ModelCtorException();
if (FAILS == Check.optional(fId, Check.range(1,50))) {
ex.add("Id is optional, 1 ..50 chars.");
}
if (FAILS == Check.required(fName, Check.range(2,50))) {
ex.add("Restaurant Name is required, 2 ..50 chars.");
}
if (FAILS == Check.optional(fLocation, Check.range(2,50))) {
ex.add("Location is optional, 2 ..50 chars.");
}
Validator[] priceChecks = {Check.range(ZERO, HUNDRED), Check.numDecimalsAlways(2)};
if (FAILS == Check.optional(fPrice, priceChecks)) {
ex.add("Price is optional, 0.00 to 100.00.");
}
if (FAILS == Check.optional(fComment, Check.range(2,50))) {
ex.add("Comment is optional, 2 ..50 chars.");
}
if ( ! ex.isEmpty() ) throw ex;
}
39. Domain Model (Managing Configurations)
public Handler getHandler(Config config) throws Exception {
final String defaultTopic = config.getString("default_topic");
boolean propagate = false;
try {
propagate = config.getBoolean("propagate");
} catch (ConfigException.Missing ignored) {
}
if ("null".equals(defaultTopic)) {
log.warn("default topic is "null"; messages will be discarded unless tagged with kt:");
}
final Properties properties = new Properties();
for (Map.Entry<String, ConfigValue> kv : config.getConfig("producer_config").entrySet()) {
properties.put(kv.getKey(), kv.getValue().unwrapped().toString());
}
final String clientId = // ..
// ..
EncryptionConfig encryptionConfig = new EncryptionConfig();
try {
Config encryption = config.getConfig("encryption");
encryptionConfig.encryptionKey = encryption.getString("key");
encryptionConfig.encryptionAlgorithm = encryption.getString("algorithm");
encryptionConfig.encryptionTransformation = encryption.getString("transformation");
encryptionConfig.encryptionProvider = encryption.getString("provider");
} catch (ConfigException.Missing ignored) {
encryptionConfig = null;
}
return new KafkaHandler(clientId, propagate, defaultTopic, producer, encryptionConfig);
}
40. Antipatterns
• Repetition
• Imperative, not expression based - hence not
composable
• Littered with exception handling code (try/catch) -
violates referential transparency
• Not modular
41. Domain Model
type ErrorOr[A] = Either[Exception, A]
private def readString(path: String, config: Config): ErrorOr[String] = try {
Either.right(config.getString(path))
} catch {
case ex: Exception => Either.left(ex)
} Step 1: Abstract exceptions with
an algebra. We need to handle
exceptions, but not throw it upstream
case class KafkaSettings(
brokers: String,
zk: String,
fromTopic: String,
toTopic: String,
errorTopic: String
)
Step 2: Use an algebraic data type
for configuration information
42. Domain Model
type ErrorOr[A] = Either[Exception, A]
private def readString(path: String, config: Config): ErrorOr[String] = try {
Either.right(config.getString(path))
} catch {
case ex: Exception => Either.left(ex)
}
import com.typesafe.config.Config
def fromKafkaConfig(config: Config) = for {
b <- readString("dcos.kafka.brokers", config)
z <- readString("dcos.kafka.zookeeper", config)
f <- readString("dcos.kafka.fromtopic", config)
t <- readString("dcos.kafka.totopic", config)
e <- readString("dcos.kafka.errortopic", config)
} yield KafkaSettings(b, z, f, t, e)
• Algebra of Monad for composition
of readString
• Looks imperative (and hence intuitive)
though in reality it’s an expression
• Reusing algebra and defining implementation
specific context
43. Domain Model
import com.typesafe.config.Config
def fromKafkaConfig(config: Config): ErrorOr[KafkaSettings] = for {
b <- readString("dcos.kafka.brokers", config)
z <- readString("dcos.kafka.zookeeper", config)
f <- readString("dcos.kafka.fromtopic", config)
t <- readString("dcos.kafka.totopic", config)
e <- readString("dcos.kafka.errortopic", config)
} yield KafkaSettings(b, z, f, t, e)
Repetitions ..
44. Algebraic Composition
def fromKafkaConfig(config: Config): ErrorOr[KafkaSettings] = for {
b <- readString("dcos.kafka.brokers", config)
z <- readString("dcos.kafka.zookeeper", config)
f <- readString("dcos.kafka.fromtopic", config)
t <- readString("dcos.kafka.totopic", config)
e <- readString("dcos.kafka.errortopic", config)
} yield KafkaSettings(b, z, f, t, e)
Either[Exception, KafkaSettings]
45. Algebraic Composition
Either[Exception, KafkaSettings]Config =>
def fromKafkaConfig: Config => ErrorOr[KafkaSettings] = (config: Config) => for {
b <- readString("dcos.kafka.brokers", config)
z <- readString("dcos.kafka.zookeeper", config)
f <- readString("dcos.kafka.fromtopic", config)
t <- readString("dcos.kafka.totopic", config)
e <- readString("dcos.kafka.errortopic", config)
} yield KafkaSettings(b, z, f, t, e)
ReaderT[ErrorOr, Config, KafkaSettings]
we want a better abstraction
for reading stuff in
the abstraction needs to
compose with Either
46. Algebraic Composition
Either[Exception, KafkaSettings]Config =>
def fromKafkaConfig: Config => ErrorOr[KafkaSettings] = (config: Config) => for {
b <- readString("dcos.kafka.brokers", config)
z <- readString("dcos.kafka.zookeeper", config)
f <- readString("dcos.kafka.fromtopic", config)
t <- readString("dcos.kafka.totopic", config)
e <- readString("dcos.kafka.errortopic", config)
} yield KafkaSettings(b, z, f, t, e)
ReaderT[ErrorOr, Config, KafkaSettings]
• An algebra abstracting our earlier
expression
• Does 2 things - the Reader monad
wraps a unary function & the T part
indicating a monad transformer composes
the 2 monads, the Reader and the Either
• ReaderT is also a monad
47. Algebraic Composition
type ConfigReader[A] = ReaderT[ErrorOr, Config, A]
def fromKafkaConfig: ConfigReader[KafkaSettings] = for {
b <- readString("dcos.kafka.brokers")
z <- readString("dcos.kafka.zookeeper")
f <- readString("dcos.kafka.fromtopic")
t <- readString("dcos.kafka.totopic")
e <- readString("dcos.kafka.errortopic")
} yield KafkaSettings(b, z, f, t, e)
def readString(path: String): ConfigReader[String] =
Kleisli { (config: Config) =>
try {
Either.right(config.getString(path))
} catch {
case ex: Exception => Either.left(ex)
}
}
48. Functional Patterns
• Reuse of already existing algebra (Reader, Monad,
Either etc.)
• Algebraic composition - form larger patterns from
smaller ones
• Abstraction remains composable
• And modular