Personal tools

Performance/Monads

From HaskellWiki

< Performance(Difference between revisions)
Jump to: navigation, search
(Using CPS for faster monads)
Line 92: Line 92:
 
<haskell>
 
<haskell>
 
instance MonadPlus MaybeCPS where
 
instance MonadPlus MaybeCPS where
mzero = MaybeCPS (\_ -> Nothing)
+
mzero = MaybeCPS (\_ -> Nothing) -- equivalent to MaybeCPS (Nothing >>=)
 
m `mplus` n = case runMaybeCPS m of
 
m `mplus` n = case runMaybeCPS m of
 
Nothing -> n
 
Nothing -> n

Revision as of 13:08, 29 October 2007

Haskell Performance Resource

Constructs:
Data Types - Functions
Overloading - FFI - Arrays
Strings - Integers - I/O
Floating point - Concurrency
Modules - Monads

Techniques:
Strictness - Laziness
Avoiding space leaks
Accumulating parameter

Implementation-Specific:
GHC - nhc98 - Hugs
Yhc - JHC

1 Unroll your MTL stacks

MTL is an excellent library for programming with monads. However stacked monad transformers do not inline well and the library is in need of an optimization pass. As a result, it can often impose a performance hit of up to 300% (your code will run up to three times slower).

If you care about this, the best option is to flatten you stack of transformers into a single, hand unrolled monad. An extreme example follows.

This is a typical MTL monad stack

  newtype DRM a = DRM {unDRM:: ErrorT Finish (RWS () DNA RNA) a}
     deriving (MonadState DNA, MonadWriter RNA, MonadError Finish, Monad)

We can unroll it as follows:

type DRM = DRMonad Finish DNA RNA
newtype DRMonad e s w a = DRMonad {runDRMonad :: s -> (Either e a,s,w)}
 
instance (Monoid m, Error e) => Monad (DRMonad e s w) where
    return x = DRMonad(\s -> (Right x, s, mempty))
    (>>=  = bindDRMonad
    fail _ = DRMonad (\s->(Left e,s,mempty))
 
{-# INLINE bindDRMonad #-}
{-# INLINE bindDRMonad2 #-}
bindDRMonad :: Monoid m => DRMonad a e s w -> (a -> DRMonad b e s w) -> DRMonad b e s w
bindDRMonad m f =  DRMonad$ \s -> case runDRMonad m s of
                  (x',s',w) ->
                    bindDRMonad2 x' (s',w,f)
bindDRMonad2 x' (s',w, f) = case x' of 
                      Left e  -> (Left e, s', w)
                      Right r -> case runDRMonad (f r) s' of
                                   (x'',s'',w') ->
                                       (x'', s'', w `mappend` w')

After this, you will also want to add the instances for MonadState, MonadWriter, etc.

2 Use Continuation Passing Style

It is well known that every monad can be "embedded" into the continuation passing monad, Cont. All that is necessary is to make the "answer type" of the Cont monad be the desired monad, e.g.

type MCPS a = forall r. Cont (M r) a
 
runMCPS m = runCont m return

Note that M's (>>=) operation isn't used. It comes up when you implement the other operations M suports.

In many cases, this will effectively avoid a layer of interpretation in much of the code using M. To see this, let's look at code that would benefit from using the Maybe monad.

liftMaybe2 :: (a -> b -> Maybe c) -> Maybe a -> Maybe b -> Maybe c
liftMaybe2 f a b =
    case a of
        Nothing -> Nothing
        Just a' -> case b of
                       Nothing -> Nothing
                       Just b' -> f a' b'

This code screams for using Maybe as a monad in which case it will look like,

liftMaybe2 f a b = do
    a' <- a
    b' <- b
    f a' b'
 
-- or just
liftMaybe2 = liftM2

One things to note about the original code is that it must constantly check the returned value even though a failure (Nothing) is most likely a rare occurrence, and further more it's possible that we will need to propagate a Nothing through an arbitrary large amount of code, though a lot of times this won't happen.

Ideally, we want "failure free" code to run as normal and only deal with failure when it occurs and immediately fail in those cases rather than propagating the failure. This is what using continuation passing style gets us.

Explicitly expanding Cont we get,

newtype MaybeCPS a = MaybeCPS { unMaybeCPS :: forall r. (a -> Maybe r) -> Maybe r }
 
runMaybeCPS m = unMaybeCPS m return
 
-- The Cont definitions of return and (>>=)
instance Monad MaybeCPS where
    return a = MaybeCPS (\k -> k a)
    MaybeCPS m >>= f = MaybeCPS (\k -> m (\a -> unMaybeCPS (f a) k))

Note that this code is just normal CPS code and completely independent of the Maybe monad. There are no case analyses. So, "failure free" code will run as normal (CPS) code.

Now we need to implement the operations.

instance MonadPlus MaybeCPS where
    mzero = MaybeCPS (\_ -> Nothing) -- equivalent to MaybeCPS (Nothing >>=)
    m `mplus` n = case runMaybeCPS m of
                      Nothing -> n
                      Just a  -> return a

There are two things to note about this code. mplus is where we use the case analysis. mplus is the only place that we look at what was returned and to do it we have to actually run the computation. This means that we only deal with "effects" when we need to. Further, mzero discards its continuation. This is the typical pattern for aborting a computation in CPS and will lead to an immediate termination of the computation.

MaybeCPS should be faster than using Maybe in most cases and should be almost a drop in replacement for Maybe. Usually, using a CPS implementation would be a drop in replacement, but Maybe is not an abstract data type. Anyway, some results for a more complicated example are here.