Mining Functional Patterns
Debasish Ghosh
@debasishg
Plan today ..
• Design Pattern

• Algebra

• Algebra <=> Functional Patterns

• Mining patterns on real world Scala code
Code ahead .. Scala code ..
.. though the principles apply
equally well to any statically typed
functional programming language ..
Solution to a Problem in Context
Design Pattern
Solution to a Problem in Context
Design Pattern
Solution to a Problem in Context
Design Pattern
Solution to a Problem in Context
Design Pattern
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)
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)
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
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
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 ..
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
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
Algebra <=> Protocol
class Monoid a where
mempty :: a
mappend :: a -> a -> a
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)
Monoid In Scala
trait Semigroup[A] {
def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
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
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
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
Functional Patterns
• Generic, reusable algebra
• Parametric on types
• Clear separation between pattern (algebra) and
its instances
• Composable through function composition
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
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 ..
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)
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
}
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 ?
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)
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
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 ?
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!
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
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!
Looking Back
• Similar problem
• Different context
• Reuse of the same algebra
• Different concrete instances of the algebra
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
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)
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
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
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;
}
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);
}
Antipatterns
• Repetition
• Imperative, not expression based - hence not
composable
• Littered with exception handling code (try/catch) -
violates referential transparency
• Not modular
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
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
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 ..
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]
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
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
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)
}
}
Functional Patterns
• Reuse of already existing algebra (Reader, Monad,
Either etc.)
• Algebraic composition - form larger patterns from
smaller ones
• Abstraction remains composable
• And modular
Thanks!

Mining Functional Patterns

  • 1.
  • 2.
    Plan today .. •Design Pattern • Algebra • Algebra <=> Functional Patterns • Mining patterns on real world Scala code
  • 3.
    Code ahead ..Scala code .. .. though the principles apply equally well to any statically typed functional programming language ..
  • 4.
    Solution to aProblem in Context Design Pattern
  • 5.
    Solution to aProblem in Context Design Pattern
  • 6.
    Solution to aProblem in Context Design Pattern
  • 7.
    Solution to aProblem in Context Design Pattern
  • 8.
    Solution to aProblem 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 anAlgebra ? 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
  • 15.
    Algebra <=> Protocol classMonoid a where mempty :: a mappend :: a -> a -> a
  • 16.
    Algebra <=> Protocolwith 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 traitSemigroup[A] { def combine(x: A, y: A): A } trait Monoid[A] extends Semigroup[A] { def empty: A }
  • 18.
    Monoid In Scala traitSemigroup[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 traitSemigroup[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 traitSemigroup[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 • Standardvocabulary - 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 // asum 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._ objectPayments { 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._ objectPayments { 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._ objectPayments { 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 • Similarproblem • 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 Paymentsextends 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 (HandlingDomain 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 (ManagingConfigurations) 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 deffromKafkaConfig(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 • Reuseof already existing algebra (Reader, Monad, Either etc.) • Algebraic composition - form larger patterns from smaller ones • Abstraction remains composable • And modular
  • 49.