Let's talk genericity or generic abstractions. In the last post we talked about an abstraction
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
Just to recapitulate, here's the definition of
Why is this a naive implementation ?
First of all it deconstructs the implementation of
On to some more reusability of generic patterns ..
Consider the following abstraction that builds on top of
1. The function does a
2. The implementation uses the
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.
Well, it's actually a concrete implementation of a generic map-reduce function ..
In fact the
And
The implementation is generic and the typesystem will ensure that the
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
Looks like our investment on an early abstraction of
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
- is generic in types
- offers operations that are completely generic on the types
- 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 ..
2 comments:
Wish you the best for your hard work, Looks like your investment on an early abstraction of mapReduce has started to pay off.
Great reading! :)
Post a Comment