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:
"(((trd1 ° forTrade) ° taxFees) ° enrichWith) ° netAmount" - Why the explicit parens? Function composition is associative.
absolutely .. updated post
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.
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.
Post a Comment