Monday, December 27, 2010

A case study of cleaner composition of APIs with the Reader monad

In my earlier post on composable domain models, I wrote about the following DSL that captures the enrichment of a security trade by computing the applicable tax/fees and then the net cash value of the trade. It uses chained composition of scalaz functors .. In this post we are going to improve upon the compositionality, introduce a new computation structure and make our APIs leaner with respect to type signatures ..

scala> trd1 ° forTrade ° taxFees ° enrichWith ° netAmount  
res0: Option[scala.math.BigDecimal] = Some(3307.5000)


Here are the building blocks for the above .. the individual functions and the type definitions for each of them ..

forTrade: Trade => (Trade, Option[List[TaxFeeId]])
taxFees: (Trade, Option[List[TaxFeeId]]) => (Trade, List[(TaxFeeId, BigDecimal)])
enrichWith: (Trade, List[(TaxFeeId, BigDecimal)]) => RichTrade
netAmount: RichTrade => Option[BigDecimal]


and here's the chaining in action with wiring made explicit ..


Note how we explicitly wire the types up so as to make the entire computation composable. Composability is a worthwhile quality to have for your abstractions. However in order for your functions to compose, the types for input and output for each of them must match. In the above example, we need to have forTrade spit out a Trade object along with the list of tax/fee id, in order for it to compose with taxFees.

For an API to be usable, the secret sauce is to make it lean. Never impose any additional burden on to your API's interface that smells of incidental complexity to the user. This is exactly what we are doing in the above composition. Note we are carrying around the Trade argument pipelining it through each of the above functions. In our use case the Trade is a read-only state and needs to be shared amongst all functions to read the information from the object.

Enter the Reader Monad

Refactor the above into the Reader monad. A Reader is meant to be used as an environment (it's also known as the Environment monad) for all the participating components of the computation. What we need to do for this is to set up a monadic structure for our computation. Here are the modified function signatures .. I have changed some of the names for better adaptability with the domain, but you get the idea ..

val forTrade: Trade => Option[List[TaxFeeId]] = //..
val taxFeeCalculate: Trade => List[TaxFeeId] => List[(TaxFeeId, BigDecimal)] = //..
val enrichTradeWith: Trade => List[(TaxFeeId, BigDecimal)] => BigDecimal = //..


Every function takes the Trade but we no longer have to do an explicit chaining by emitting the Trade also as an output. This is where a monad shines. A monad gives you a shared interface to many libraries where you don't need to implement sequencing explicitly within your DSELs.

And here's our DSEL that runs through the sequence of enriching a trade while using the passed in trade as an environment .. (thanks @runarorama for the help with the Reader in scalaz)

val enrich = for {
  taxFeeIds      <- forTrade        // get the tax/fee ids for a trade
  taxFeeValues   <- taxFeeCalculate // calculate tax fee values
  netAmount      <- enrichTradeWith // enrich trade with net amount
}
yield((taxFeeIds ° taxFeeValues) ° netAmount)


This is a comprehension in Scala which is like the do notation of Haskell. Desugar it as an exercise and explore how flatMap does the sequencing.

Here's what the type of enrich looks like ..

scala> enrich
res1: (net.debasishg.domain.trade.dsl.TradeModel.Trade) => Option[BigDecimal] = <function1>


enrich is monadic in nature and follows the usual structure of a monad that sequences its operations through bind to give it an imperative look and feel. If any of the above sub-computations fail, the whole computation fails. But show it to a person who knows the domain of security trading - the steps in enrich nicely models the ubiquitous language.

I have the entire DSL in my github repo. You can get the use of enrich here in the test case ..

4 comments:

Tom Crockett said...

"(((trd1 ° forTrade) ° taxFees) ° enrichWith) ° netAmount" - Why the explicit parens? Function composition is associative.

Unknown said...

absolutely .. updated post

Anonymous said...

Hi Debasish!
I like your posts on Domain models and scalaz, it needs a lot more attention IMHO.
I was seeing through your repo and came to this:
https://github.com/debasishg/tryscalaz/blob/master/src/main/scala/TradeModel.scala

on Line 49 there is this method:
def validQuantity(qty: BigDecimal): Validation[String, BigDecimal] =
try {
if (qty <= 0) "qty must be > 0".fail
else if (qty > 500) "qty must be <= 500".fail
else qty.success
} catch {
case e => e.toString.fail
}

You use here a try{} catch block. Could you explain why? Because the only exception I could ever imagine that COULD arise here is a NullpointerException if qty is null.

So my Questions:
Is the NPE the only Exception you try to catch here? If not what am I missing?

IF the block is only guarding for an NPE why not use a plain == null Check or some "Option" handling?

Looking forward to your answer.

regards Andreas S.

Unknown said...

Hi Andreas -

You are correct. It doesn't make sense in the current context. Initially I was taking a string arg to the function and the try/catch block came in for NumberFormatException. Thanks for pointing this out - will remove when I have some time.

Thanks.