Monday, October 04, 2010

Domain Modeling in Haskell - Follow the Types

In my last post I had talked about how implementing the same problem in different programming languages make you think differently. Of course these languages also have to be sufficiently opinionated to make you think in terms of their idioms and best practices. In that post I had discussed Scala and Clojure, two of the most competing languages trying to get the programmer mindshare on the JVM today.

As a weekend exercise I tried the same modeling problem in another functional language, that's touted to be the purest around. In my earlier exercise, Scala was statically typed, offered both functional and OO paradigms to model your domain. So it was somewhat a matter of choice to remain functional or fall down to objects for implementing your model. Clojure is functional, but still has matured in 1.2 to offer some features that you can use to build your model around OOish abstractions. Whether that's idiomatic Clojure or not, I will leave to the purists, but my friend Sergio has implemented a domain model using dynamic mixins in Clojure. A nice idea with deftypes and defprotocols.

With Haskell you don't have any escape route - you've to be functional and model your domain artifacts as functions that compose. With Haskell way of thinking you think in types, keep your pure functional code separate from impure side-effecting logic and use the myriads of ways to compose your computations. Without further ado ..

Here's the Trade abstraction as a Haskell data type .. once again much simpler compared to a real world one ..

-- some helpful synonyms
type Instrument = String
type Account = String
type NetAmount = Double

-- various types of tax and fees
data TaxFeeId = TradeTax | Commission | VAT deriving (Show, Eq, Ord)

data Trade = Trade {
      account     :: Account
     ,instrument  :: Instrument
     ,ref_no      :: String
     ,unit_price  :: Double
     ,quantity    :: Double
} deriving (Show)

We can define a helper function that gets me the principal of a trade ..

principal :: Trade -> Double
principal trade = (unit_price trade) * (quantity trade)

As in our previous exercise, we will try to decorate a trade with an appropriate set of tax and fee components. Remember we used mixins for Scala and function composition in Clojure that composes the decorators along with the subject.

The name Decorator nicely abstracts the intent of the computation. In some languages the implementation is a bit ceremonious, in others it's just a natural idiom of the language. With Haskell implementation we will also rely on composition, but we will try to get a nice pointfree computation that also makes a good DSL.

Let's find the set of tax and fees applicable for a trade. It depends on a few of the attributes of the trade and is usually computed through a somewhat detailed business logic. For our purposes, we will keep it simple and return a fixed set of tax/fee ids for our trade.

-- tax and fees applicable for a trade
forTrade :: Trade -> (Trade, [TaxFeeId])
forTrade trade =
    (trade, [TradeTax, Commission, VAT])

forTrade gives us a list of tax/fee ids applicable for the trade. However, we need to compute the value of each of them before we could use them to enrich our trade. We have the combinator taxFees that does exactly this and returns an association list containing pairs of tax/fee ids and their computed values for the trade.

taxFees use a function valueAs that implements Haskell's pattern matching. Note how the implementation is self explanatory through using algebraic data types of Haskell.

Note taxFees consume input the same way as forTrade outputs .. we are preparing to set up the composition ..

taxFees :: (Trade, [TaxFeeId]) -> (Trade, [(TaxFeeId, Double)])
taxFees (trade, taxfeeids) =
    (trade, zip taxfeeids (map (valueAs trade) taxfeeids))

-- valuation of tax and fee
valueAs :: Trade -> TaxFeeId -> Double
valueAs trade TradeTax = 0.2 * (principal trade)
valueAs trade Commission = 0.15 * (principal trade)
valueAs trade VAT = 0.1 * (principal trade)

Now we are ready for our final function that will enrich the trade. But before that let's define another type for our rich trade, that will contain a trade along with a Map of tax and fees.

import qualified Data.Map as M

type TaxFeeMap = M.Map TaxFeeId Double

data RichTrade = RichTrade {
      trade        :: Trade 
     ,taxFeeMap    :: TaxFeeMap 
} deriving (Show)

Our enrichWith function simply creates a Map out of the association list and tags it along with the Trade to form a RichTrade. Note how we use combinators like foldl to abstract the iteration and populate the Map.

-- trade enrichment
enrichWith :: (Trade, [(TaxFeeId, Double)]) -> RichTrade
enrichWith (trade, taxfees) = 
    RichTrade trade (foldl(\map (k, v) -> M.insert k v map) M.empty taxfees)

When we talk about enriching a trade in the problem domain, we talk about 3 steps - get the list of tax fees, compute the values for each of them for the particular trade and form the rich trade. Our Haskell implementation is just as verbose for the user. The enrichment recipe using the above functions boils down to the following pointfree composition ..

enrichWith . taxFees . forTrade

bah! Now how do we compute the net amount of a trade using the above model. Easy. Just define another combinator that can plug in with the above expression ..

netAmount :: RichTrade -> NetAmount
netAmount rtrade = 
    let t = trade rtrade
        p = principal t
        m = taxFeeMap rtrade
    in M.fold (\v a -> a + v) p m

and now you can compute the net amount as ..

netAmount . enrichWith . taxFees . forTrade

And we get an EDSL for free without any additional effort and just by sticking to idiomatic Haskell. The type system guides you in every respect - Haskellers call it "Follow the types". And once you have a compiled version of your code, it's type safe. If you have designed your abstractions based on the problem domain model, you have already implemented a lot of the domain rules out there.

As far as Haskell type system is concerned we have just scratched the surface here in this simple example. The idea was to compare with the implementations that Scala and Clojure had in my earlier post. In an upcoming post, I will take this example and enhance it with some more domain logic that will allow us to unearth some more of Haskell power, like type classes, a few monads etc. Incidentally there's a Reader monad begging to be modeled in the above example - can you spot it ?

1 comment:

Anonymous said...

A couple thoughts:

M.fold (\v a -> a + v) can be replaced with M.fold (+).

foldl(\map (k, v) -> M.insert k v map) M.empty can be replaced with M.fromList.

As an exercise, I tried rewriting your code using a Reader monad, but in the end (for this limited problem) I found it to over-complicate things. Instead, I simplified things differently:

feeRate :: TaxFeeId -> Double
feeRate TradeTax = 0.2
feeRate Commission = 0.15
feeRate VAT = 0.1

buildTaxFeeMap :: Trade -> TaxFeeMap
buildTaxFeeMap trade =
M.fromList [(f, feeRate f * principal trade) | f <- [TradeTax .. ]]

netAmount :: RichTrade -> NetAmount
netAmount rtrade = M.fold (+) (principal $ trade rtrade) (taxFeeMap rtrade)

main = print $ netAmount richtrade where
richtrade = RichTrade trade $ buildTaxFeeMap trade
trade = Trade "acct" "INST" "ref123" 10.0 20.0

(Note that you have to add "Enum" to the derived classes of TaxFeeId for this code to work.)

[Sorry for the bad formatting -- tags "pre" and "code" are not accepted.]