Sunday, June 25, 2017

Domain Models - Early Abstractions and Polymorphic Domain Behaviors

Let's talk genericity or generic abstractions. In the last post we talked about an abstraction Money, which, BTW was not generic. But we expressed some of the operations on Money in terms of a Money[Monoid], where Monoid is a generic algebraic structure. By algebraic we mean that a Monoid

  1. is generic in types
  2. offers operations that are completely generic on the types
  3. all operations honor the algebraic laws of left and right identities and associativity

But when we design a domain model, what does this really buy us ? We already saw in the earlier post how law abiding abstractions save you from writing some unit tests just through generic verification of the laws using property based testing. That's just a couple of lines in any of the available libraries out there.

Besides reducing the burden of your unit tests, what does Money[Monoid] buy us in the bigger context of things ? Let's look at a simple operation that we defined in Money ..

Just to recapitulate, here's the definition of Money

class Money (val items: Map[Currency, BigDecimal]) { //..
}

object Money {
  final val zeroMoney = new Money(Map.empty[Currency, BigDecimal])

  def apply(amount: BigDecimal, ccy: Currency) = new Money(Map(ccy -> amount))

  // concrete naive implementation: don't
  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) 
      }
    )

  //..
}

add is a naive implementation though it's possibly the most frequent one that you will ever encounter in domain models around you. It picks up the Map elements and then adds the ones with the same key to come up with the new Money.

Why is this a naive implementation ?

First of all it deconstructs the implementation of Money, instead of using the algebraic properties that the implementation may have. Here we implement Money in terms of a Map, which itself forms a Monoid under the operations defined by Monoid[Map[K, V]]. Hence why don't we use the monoidal algebra of a Map to implement the operations of Money ?

object Money {

  //..

  def add(m: Money, n: Money) = new Money(m.items |+| n.items)

  //..
}

|+| is a helper function that combines the 2 Maps in a monoidal manner. The concrete piece of code that you wrote in the naive implementation is now delegated to the implementation of the algebra of monoids for a Map in a completely generic way. The advantage is that you need (or possibly someone else has already done that for you) to write this implementation only once and use it in every place you use a Map. Reusability of polymorphic code is not via documentation but by actual code reuse.

On to some more reusability of generic patterns ..

Consider the following abstraction that builds on top of Money ..

import java.time.OffsetDateTime
import Money._

import cats._
import cats.data._
import cats.implicits._

object Payments {
  case class Account(no: String, name: String, openDate: OffsetDateTime, 
    closeDate: Option[OffsetDateTime] = None)
  case class Payment(account: Account, amount: Money, dateOfPayment: OffsetDateTime)

  // returns the Money for credit payment, zeroMoney otherwise
  def creditsOnly(p: Payment): Money = if (p.amount.isDebit) zeroMoney else p.amount

  // compute valuation of all credit payments
  def valuation(payments: List[Payment]) = payments.foldLeft(zeroMoney) { (a, e) =>
    add(a, creditsOnly(e))
  }
  //..
}


valuation gives a standard implementation folding over the List that it gets. Now let's try to critique the implementation ..

1. The function does a foldLeft on the passed in collection payments. The collection only needs to have the ability to be folded over and List can do much more than that. We violate the principle of using the least powerful abstraction as part of the implementation. The function that implements the fold over the collection only needs to take a Foldable - that prevents misuse on part of a user feeling like a child in a toy store with something more grandiose than what she needs.

2. The implementation uses the add function of Money, which is nothing but a concrete wrapper over a monoidal operation. If we can replace this with something more generic then it will be a step forward towards a generic implementation of the whole function.

3. If we squint a bit, we can get some more light into the generic nature of all the components of this 2 line small implementation. zeroMoney is a zero of a Monoid, fold is a generic operation of a Foldable, add is a wrapper over a monoidal operation and creditsOnly is a mapping operation over every payment that the collection hands you over. In summary the implementation folds over a Foldable mapping each element using a function and uses the monoidal operation to collapse the fold.

Well, it's actually a concrete implementation of a generic map-reduce function ..

def mapReduce[F[_], A, B](as: F[A])(f: A => B)
  (implicit fd: Foldable[F], m: Monoid[B]): B = 
    fd.foldLeft(as, m.empty)((b, a) => m.combine(b, f(a)))

In fact the Foldable trait contains this implementation in the name of foldMap, which makes our implementation of mapReduce even simpler ..

def mapReduce1[F[_], A, B](as: F[A])(f: A => B)
  (implicit fd: Foldable[F], m: Monoid[B]): B = fd.foldMap(as)(f)

And List is a Foldable and our implementation of valuation becomes as generic as ..

object Payments {
  //..

  // generic implementation
  def valuation(payments: List[Payment]): Money = {
    implicit val m: Monoid[Money] = Money.MoneyAddMonoid
    mapReduce(payments)(creditsOnly)
  }
}

The implementation is generic and the typesystem will ensure that the Money that we produce can only come from the list of payments that we pass. In the naive implementation there's always a chance that the user subverts the typesystem and can play malice by plugging in some additional Money as the output. If you look at the type signature of mapReduce, you will see that the only way we can get a B is by invoking the function f on an element of F[A]. Since the function is generic on types we cannot ever produce a B otherwise. Parametricity FTW.

mapReduce is completely generic on types - there's no specific implementation that asks it to add the payments passed to it. This abstraction over operations is provided by the Monoid[B]. And the abstraction over the form of collection is provided by Foldable[F]. It's now no surprise that we can pass in any concrete operation or structure that honors the contracts of mapReduce. Here's another example from the same model ..

object Payments {
  //..

  // generic implementation
  def maxPayment(payments: List[Payment]): Money = {
    implicit val m: Monoid[Money] = Money.MoneyOrderMonoid
    mapReduce(payments)(creditsOnly)
  }
}

We want to compute the maximum credit payment amount from a collection of payments. A different domain behavior needs to be modeled but we can think of it as belonging to the same form as valuation and implemented using the same structure as mapReduce, only passing a different instance of Monoid[Money]. No additional client code, no fiddling around with concrete data types, just matching the type contracts of a polymorphic function.

Looks like our investment on an early abstraction of mapReduce has started to pay off. The domain model remains clean with much of the domain logic being implemented in terms of the algebra that the likes of Foldables and Monoids offer. I discussed some of these topics at length in my book Functional and Reactive Domain Modeling. In the next instalment we will explore some more complex algebra as part of domain modeling ..

Sunday, June 18, 2017

Domain models, Algebraic laws and Unit tests

In a domain model, when you have a domain element that forms an algebraic abstraction honoring certain laws, you can get rid of many of your explicitly written unit tests just by checking the laws. Of course you have to squint hard and discover the lawful abstraction that hides behind your concrete domain element.

Consider this simple abstraction for Money that keeps track of amounts in various currencies.

scala> import Money._
import Money._

// 1000 USD
scala> val m = Money(1000, USD)
m: laws.Money = (USD,1000)

// add 248 AUD
scala> val n = add(m, Money(248, AUD))
n: laws.Money = (AUD,248),(USD,1000)

// add 230 USD more
scala> val p = add(n, Money(230, USD))
p: laws.Money = (AUD,248),(USD,1230)

// value of the money in base currency (USD)
scala> p.toBaseCurrency
res1: BigDecimal = 1418.48

// debit amount
scala> val q = Money(-250, USD)
q: laws.Money = (USD,-250)

scala> val r = add(p, q)
r: laws.Money = (AUD,248),(USD,980)

The valuation of Money is done in terms of its base currency which is usually USD. One of the possible implementations of Money is the following (some parts elided for future explanations) ..

sealed trait Currency
case object USD extends Currency
case object AUD extends Currency
case object JPY extends Currency
case object INR extends Currency

class Money private[laws] (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
    }

  override def toString = items.toList.mkString(",")
}

object Money {
  final val zeroMoney = new Money(Map.empty[Currency, BigDecimal])

  def apply(amount: BigDecimal, ccy: Currency) = new Money(Map(ccy -> amount))
  def add(m: Money, amount: BigDecimal, ccy: Currency) = ???

  final val exchangeRateWithUSD: Map[Currency, BigDecimal] = 
    Map(AUD -> 0.76, JPY -> 0.009, INR -> 0.016, USD -> 1.0)
}

Needless to say we will have quite a number of unit tests that check for addition of Money, including the boundary cases of adding to zeroMoney.

It's not very hard to see that the type Money forms a Monoid under the add operation. Or to speak a bit loosely we can say that Money is a Monoid under the add operation.

A Monoid has laws that every instance needs to honor - associativity, left identity and right identity. And when your model element needs to honor the laws of algebra, it's always recommended to include the verification of the laws as part of your test suite. Besides validating the sanity of your abstractions, one side-effect of verifying laws is that you can get rid of many of your explicitly written unit tests for the operation that forms the Monoid. They will be automatically verified when verifying the laws of Monoid[Money].

Here's how we define Monoid[Money] using Cats ..

val MoneyAddMonoid: Monoid[Money] = new Monoid[Money] {
  def combine(m: Money, n: Money): Money = add(m, n)
  def empty: Money = zeroMoney
}

and the implementation of the previously elided add operation on Money using Monoid on Map ..

object Money {
  //..

  def add(m: Money, amount: BigDecimal, ccy: Currency) = 
    new Money(m.items |+| Map(ccy -> amount))

  //..

}

Now we can verify the laws of Monoid[Money] using specs2 and ScalaCheck and the helper classes that Cats offers ..

import cats._
import kernel.laws.GroupLaws
import org.scalacheck.{ Arbitrary, Gen }
import Arbitrary.arbitrary

class MoneySpec extends CatsSpec { def is = s2"""

  This is a specification for validating laws of Money

  (Money) should
     form a monoid under addition    $e1 
  """

  implicit lazy val arbCurrency: Arbitrary[Currency] = Arbitrary { 
    Gen.oneOf(AUD, USD, INR, JPY) 
  }

  implicit def moneyArbitrary: Arbitrary[Money] = 
    Arbitrary {
      for {
        i <- Arbitrary.arbitrary[Map[Currency, BigDecimal]]
      } yield new Money(i)
    }

  def e1 = checkAll("Money", GroupLaws[Money].monoid(Money.MoneyAddMonoid))
}


and running the test suite will verify the Monoid laws for Monoid[Money] ..

[info] This is a specification for validating laws of Money
[info]
[info] (Money) should
[info] form a monoid under addition monoid laws must hold for Money
[info] + monoid.associativity
[info] + monoid.combineAll
[info] + monoid.combineAll(Nil) == id
[info] + monoid.combineAllOption
[info] + monoid.combineN(a, 0) == id
[info] + monoid.combineN(a, 1) == a
[info] + monoid.combineN(a, 2) == a |+| a
[info] + monoid.isEmpty
[info] + monoid.leftIdentity
[info] + monoid.rightIdentity
[info] + monoid.serializable

In summary ..
  • strive to find abstractions in your domain model that are constrained by algebraic laws
  • check all laws as part of your test suite
  • you will find that you can get rid of quite a few explicitly written unit tests just by checking the laws of your abstraction
  • and of course use property based testing for unit tests
In case you want to take a look at the full code base, it's there on my Github repo. In the next post we will take the next step towards modeling with generic algebraic code using the Monoid pattern from this example. Code written in parametric form without depending on specialized concrete types can be more robust, easier to test and easier to reason about. I have also discussed this at length in my book Functional and Reactive Domain Modeling. I plan to supplement the materials covered there with more examples and code patterns ..