Trade
from an association list. We used monadic lifts (liftM
) and monadic apply (ap
) to chain our builder that builds up the Trade data. Here's what we did ..makeTrade :: [(String, Maybe String)] -> Maybe Trade
makeTrade alist =
Trade `liftM` lookup1 "account" alist
`ap` lookup1 "instrument" alist
`ap` (read `liftM` (lookup1 "market" alist))
`ap` lookup1 "ref_no" alist
`ap` (read `liftM` (lookup1 "unit_price" alist))
`ap` (read `liftM` (lookup1 "quantity" alist))
lookup1 key alist = case lookup key alist of
Just (Just s@(_:_)) -> Just s
_ -> Nothing
Immediately after the post, I got some useful feedback on Twitter suggesting the use of applicatives instead of monads. A Haskell newbie, that I am, this needed some serious explorations into the wonderful world of functors and applicatives. In this post let's explore some of the goodness that applicatives offer, how using applicative style of programming encourages a more functional feel and why you should always use applicatives unless you need the special power that monads offer.
Functors and Applicatives
In Haskell a functor is a typeclass defined as
class Functor f where
fmap :: (a -> b) -> f a -> f b
fmap lifts a pure function into a computational context. For more details on functors, applicatives and all of typeclasses, refer to the Typeclassopedia that Brent Yorgey has written. In the following text, I will be using examples from our domain model of securities trading. After all, I am trying to explore how Haskell can be used to build expressive domain models with a very popular domain at hand.
Consider the association list
rates
from our domain model in my last post, which stores pairs of tax/fee and the applicable rates as percentage on the principal.*Main> rates
[(TradeTax,0.2),(Commission,0.15),(VAT,0.1)]
We would like to increase all rates by 10%.
fmap
is our friend here ..*Main> fmap (\(tax, amount) -> (tax, amount * 1.1)) rates
[(TradeTax,0.22000000000000003),(Commission,0.165),(VAT,0.11000000000000001)]
This works since
List
is an instance of the Functor
typeclass. The anonymous function gets lifted into the computational context of the List
data type. But the code looks too verbose, since we have to destructure the tuple within the anonymous function and thread the increment logic manually within it. We can increase the level of abstraction by making the tuple itself a functor. In fact it's so in Haskell and the function gets applied to the second component of the tuple. Here's the more idiomatic version ..*Main> ((1.1*) <$>) <$> rates
[(TradeTax,0.22000000000000003),(Commission,0.165),(VAT,0.11000000000000001)]
Note how we lift the function across 2 levels of functors - a list and within that, a tuple. fmap does the magic! The domain model becomes expressive through the power of Haskell's higher level of abstractions.
Applicatives add more power to functors. While functors lift pure functions, with applicatives you can lift functions from one context into another. Here's how you define the
Applicative
typeclass.class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Note
pure
is the equivalent of a return
in monads, while <*>
equals ap
. Control.Applicative
also defines a helper function <$>
, which is basically an infix version of fmap
. The idea is to help write functions in applicative style.(<$>) :: Applicative f => (a -> b) -> f a -> f b
Follow the types and see that
f <$> u
is the same as pure f <*> u
.Note also the following equivalence of
fmap
and <$>
..*Main> fmap (+3) [1,2,3,4]
[4,5,6,7]
*Main> (+3) <$> [1,2,3,4]
[4,5,6,7]
Using the applicative style our
makeTrade
function becomes the following ..makeTrade :: [(String, Maybe String)] -> Maybe Trade
makeTrade alist =
Trade <$> lookup1 "account" alist
<*> lookup1 "instrument" alist
<*> (read <$> (lookup1 "market" alist))
<*> lookup1 "ref_no" alist
<*> (read <$> (lookup1 "unit_price" alist))
<*> (read <$> (lookup1 "quantity" alist))
Can you figure out how the above invocation works ? As I said before, follow the types ..
Trade
is a pure function and <$>
lifts Trade
onto the first invocation of lookup1
, which is a Maybe
functor. The result is another Maybe
, which BTW is an Applicative functor as well. Then the rest of the chain continues through partial application and an applicative lifting from one context to another.Why choose Applicatives over Monads ?
One reason is that there are more applicatives than monads. Monads are more powerful - using
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
you can influence the structure of your overall computation, while with applicatives your structure remains fixed. You only get sequencing of effects with an applicative functor. As an exercise try exploring the differences in the behavior of makeTrade
function implemented using monadic lifts and applicatives when lookup1
has some side-effecting operations. Conor McBride and Ross Paterson has a great explanation in their functional pearl paper Applicative Programming with Effects. Applicatives being more in number, you have more options of abstracting your domain model.In our example domain model, suppose we have the list of tax/fees and the list of rates for each of them. And we would like to build our rates data structure.
ZipList
applicative comes in handy here .. ZipList
is an applicative defined as follows ..instance Applicative ZipList where
pure x = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)
*Main> getZipList $ (,) <$> ZipList [TradeTax, Commission, VAT] <*> ZipList [0.2, 0.15, 0.1]
[(TradeTax,0.2),(Commission,0.15),(VAT,0.1)]
ZipList
is an applicative and NOT a monad.Another important reason to choose applicatives over monads (but only when possible) is that applicatives compose, monads don't (except for certain pairs). The McBride and Paterson paper has lots of discussions on this.
Finally programs written with applicatives often have a more functional feel than some of the monads with the
do
notation (that has an intrinsically imperative feel). Have a look at the following snippet which does the classical do-style first and then follows it up with the applicative style using applicative functors.-- classical imperative IO
notice = do
trade <- getTradeStr
forClient <- getClientStr
putStrLn $ "Trade " ++ trade ++ forClient
-- using applicative style
notice = do
details <- (++) <$> getTradeStr <*> getClientStr
putStrLn $ "Trade " ++ details
McBride and Paterson has the final say on how to choose monads or applicatives in your design .. "The moral is this: if you’ve got an Applicative functor, that’s good; if you’ve also got a Monad, that’s even better! And the dual of the moral is this: if you want a Monad, that’s good; if you only want an Applicative functor, that’s even better!"
Applicative Functors for Expressive Business Rules
As an example from our domain model, we can write the following applicative snippet for calculating the net amount of a trade created using
makeTrade
..*Main> let trd = makeTrade [("account", Just "a-123"), ("instrument", Just "IBM"),
("market", Just "Singapore"), ("ref_no", Just "r-123"),
("unit_price", Just "12.50"), ("quantity", Just "200" )]
*Main> netAmount <$> enrichWith . taxFees . forTrade <$> trd
Just 3625.0
Note how the chain of functions get lifted into trd (
Maybe Trade
) that's created by makeTrade
. This is possible since Maybe
is an applicative functor. The beauty of applicative functors is that you can abstract this lifting into any of them. Let's lift the chain of invocation into a list of trades generating a list of net amount values for each of them. Remember List
is also another applicative functor in Haskell. For a great introduction to applicatives and functors, go read Learn Yourself a Haskell for Great Good.*Main> (netAmount <$> enrichWith . taxFees . forTrade <$>) <$> [trd2, trd3]
[Just 3625.0,Just 3375.0]
Look how we have the minimum of syntax with this applicative style. This makes business rules very expressive and not entangled into a maze of accidental complexity.
6 comments:
You forget telling readers that <$> is the operator for infix fmap before <$> shows up.
I'm not familiar with Applicatives...
I was wondering how one would write a variation of your example of increasing all rates by 10%:
((1.1*) <$>) <$> rates
that would increase tax by 10%.
Sergio
@Anonymous ..
((1.1*) <$> ) <$> (filter(\(x, _) -> x == TradeTax) rates)
I like that ((->) r) is also a Functor instance, so we can
re-define (.) to be fmap and get function composition for
free. In GHCi:
λ> let f . g = fmap f g
λ> :t (.)
(.) :: (Functor f) => (a -> b) -> f a -> f b
The following works fine:
λ> ((*4) . (+4)) . [1..5]
[20,24,28,32,36]
So instead of,
λ> ((1.1*)<$>) <$> [Just 5,Just 2]
[Just 5.5,Just 2.2]
you can write
λ> ((1.1*).) . [Just 5,Just 2]
[Just 5.5,Just 2.2]
Pretty nice.
Instead of all those references to 'alist', you could have defined 'lookup x = lookup1 x alist' in a where clause inside mkTrade. That would make it cleaner IMHO.
@Christopher: I would advise against your approach. It is useful to observe that the fmap instance for ((->) r) is (.). On the other hand, you have shown the opposite: redefining (.) as fmap is not the same at all, and highly confusing.
Post a Comment