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