My last two blog posts have been about endomorphisms and how it combines with the other functional structures to help you write expressive and composable code. In A DSL with an Endo - monoids for free, endos play with Writer monad and implement a DSL for a sequence of activities through monoidal composition. And in An exercise in Refactoring - Playing around with Monoids and Endomorphisms, I discuss a refactoring exercise that exploits the monoid of an endo to make composition easier. Endomorphisms help you lift your computation into a data type that gives you an instance of a monoid. And thea good title for a possible blog post .. endo is the new fluent API ..
— Debasish Ghosh (@debasishg) June 1, 2013
mappend
operation of the monoid is the function composition. Hence once you have the Endo
for your type defined, you get a nice declarative syntax for the operations that you want to compose, resulting in a fluent API.
Just a quick recap .. endomorphisms are functions that map a type on to itself and offer composition over monoids. Given an endomorphism we can define an implicit monoid instance ..
implicit def endoInstance[A]: Monoid[Endo[A]] = new Monoid[Endo[A]] { def append(f1: Endo[A], f2: => Endo[A]) = f1 compose f2 def zero = Endo.idEndo }I am not going into the details of this, which I discussed at length in my earlier posts. In this article I will sum up with yet another use case for making fluent APIs using the monoid instance of an Endo. Consider an example from the domain of securities trading, where a security trade goes through a sequence of transformations in its lifecycle through the trading process .. Here's a typical
Trade
model (very very trivialified for demonstration) ..
sealed trait Instrument case class Security(isin: String, name: String) extends Instrument case class Trade(refNo: String, tradeDate: Date, valueDate: Option[Date] = None, ins: Instrument, principal: BigDecimal, net: Option[BigDecimal] = None, status: TradeStatus = CREATED)Modeling a typical lifecycle of a trade is complex. But for illustration, let's consider these simple ones which need to executed on a trade in sequence ..
- Validate the trade
- Assign value date to the trade, which will ideally be the settlement date
- Enrich the trade with tax/fees and net trade value
- Journalize the trade in books
Trade
and return a copy of the Trade
with some attributes modified. A naive way of doing that will be as follows .. def validate(t: Trade): Trade = //.. def addValueDate(t: Trade): Trade = //.. def enrich(t: Trade): Trade = //.. def journalize(t: Trade): Trade = //..and invoke these methods in sequence while modeling the lifecycle. Instead we try to make it more composable and lift the function
Trade => Trade
within the Endo
.. type TradeLifecycle = Endo[Trade]and here's the implementation ..
// validate the trade: business logic elided def validate: TradeLifecycle = ((t: Trade) => t.copy(status = VALIDATED)).endo // add value date to the trade (for settlement) def addValueDate: TradeLifecycle = ((t: Trade) => t.copy(valueDate = Some(t.tradeDate), status = VALUE_DATE_ADDED)).endo // enrich the trade: add taxes and compute net value: business logic elided def enrich: TradeLifecycle = ((t: Trade) => t.copy(net = Some(t.principal + 100), status = ENRICHED)).endo // journalize the trade into book: business logic elided def journalize: TradeLifecycle = ((t: Trade) => t.copy(status = FINALIZED)).endoNow endo has an instance of
Monoid
defined by scalaz and the mappend
of Endo
is function composition .. Hence here's our lifecycle model using the holy monoid of endo .. def doTrade(t: Trade) = (journalize |+| enrich |+| addValueDate |+| validate).apply(t)It's almost the specification that we listed above in numbered bullets. Note the inside out sequence that's required for the composition to take place in proper order.
Why not plain old composition ?
A valid question. The reason - abstraction. Abstracting the composition within types helps you compose the result with other types, as we saw in my earlier blog posts. In one of them we built larger abstractions using the Writer monad with
Endo
and in the other we used the mzero
of the monoid as a fallback during composition thereby avoiding any special case branch statements.
One size doesn't fit all ..
The endo and its monoid compose beautifully and gives us a domain friendly syntax that expresses the business functionality ina nice succinct way. But it's not a pattern which you can apply everywhere where you need to compose a bunch of domain behaviors. Like every idiom, it has its shortcomings and you need different sets of solutions in your repertoire. For example the above solution doesn't handle any of the domain exceptions - what if the validation fails ? With the above strategy the only way you can handle this situation is to throw exceptions from validate function. But exceptions are side-effects and in functional programming there are more cleaner ways to tame the evil. And for that you need different patterns in practice. More on that in subsequent posts ..
2 comments:
We can generalise Endo[A] to Kleisli[Id, A, A] (where Id is the identity monad). (In fact, we can generalise it further to arbitrary categories, but Kleisli alone provides a lot of power.)
For Kleisli[F, A, A] when F is a monad, there is a monoid where mappend is kleisli composition, and mzero is the kleisli arrow corresponding to the function (a: A) => a.point[F]. When F is Id, this gives us the same behaviour as the monoid for Endo[A].
But we can now choose F to be any monad, allowing us to add error handling, reader, writer, state etc. to the computation, while retaining the fluent-like simplicity of monoidal addition.
As is often the case in functional programming, generalising the concept reveals something more powerful; really, the OO version of "fluent APIs" are quite restricted and not all that expressive compared to simple combinators like |+|.
(Apologies if this is pre-empting a future post!)
This is cool!
Post a Comment