Conditional composition of functions by Mark Seemann
A major benefit of Functional Programming is the separation of decisions and (side-)effects. Sometimes, however, one decision ought to trigger an impure operation, and then proceed to make more decisions. Using functional composition, you can succinctly conditionally compose functions.
In my article on how Functional Architecture falls into the Ports and Adapters pit of success, I describe how Haskell forces you to separate concerns:
- Your Domain Model should be pure, with business decisions implemented by pure functions. Not only does it make it easier for you to reason about the business logic, it also has the side-benefit that pure functions are intrinsically testable.
- Side-effects, and other impure operations (such as database queries) can be isolated and implemented as humble functions.
- Read some data using an impure query.
- Pass that data to a pure function.
- Use the return value from the pure function to perform various side-effects. You could, for example, write data to a database, send an email, or update a user interface.
Caravans for extra space #
Based on my previous restaurant-booking example, Martin Rykfors suggests "a new feature request. The restaurant has struck a deal with the local caravan dealership, allowing them to rent a caravan to park outside the restaurant in order to increase the seating capacity for one evening. Of course, sometimes there are no caravans available, so we'll need to query the caravan database to see if there is a big enough caravan available that evening:"
findCaravan :: ServiceAddress -> Int -> ZonedTime -> IO (Maybe Caravan)
findCaravan is a slight modification of the function Martin suggests, because I imagine that the caravan dealership exposes their caravan booking system as a web service, so the function needs a service address as well. This change doesn't impact the proposed solution, though.
This problem definition fits the above general problem statement: you'd only want to call the
findCaravan function if
That's still a business decision, so you ought to implement it as a pure function. If you (for a moment) imagine that you have a
Maybe Caravan instead of an
IO (Maybe Caravan), you have all the information required to make that decision:
checkCaravanCapacityOnError :: Error -> Maybe Caravan -> Reservation -> Either Error Reservation checkCaravanCapacityOnError CapacityExceeded (Just caravan) reservation = if caravanCapacity caravan < quantity reservation then Left CapacityExceeded else Right reservation checkCaravanCapacityOnError err _ _ = Left err
Notice that this function not only takes
Maybe Caravan, it also takes an
Error value. This encodes into the function's type that you should only call it if you have an
Error that originates from a previous step. This
Error value also enables the function to only check the caravan's capacity if the previous
Error was a
Error can also be
ValidationError, in which case there's no reason to check the caravan's capacity.
This takes care of the Domain Model, but you still need to figure out how to get a
Maybe Caravan value. Additionally, if
Right Reservation, you'd probably want to reserve the caravan for the evening. You can imagine that this is possible with the following function:
reserveCaravan :: ServiceAddress -> ZonedTime -> Caravan -> IO ()
This function reserves the caravan at the supplied time. In order to keep the example simple, you can imagine that the provided
ZonedTime indicates an entire day (or evening), and not just an instant.
Composition of caravan-checking #
As a first step, you can compose an impure function that
- Queries the caravan dealership for a caravan
- Calls the pure
- Reserves the caravan if the return value was
import Control.Monad (forM_) import Control.Monad.Trans (liftIO) import Control.Monad.Trans.Either (EitherT(..), hoistEither) checkCaravan :: Reservation -> Error -> EitherT Error IO Reservation checkCaravan reservation err = do c <- liftIO $ findCaravan svcAddr (quantity reservation) (date reservation) newRes <- hoistEither $ checkCaravanCapacityOnError err c reservation liftIO $ forM_ c $ reserveCaravan svcAddr (date newRes) return newRes
It starts by calling
findCaravan by closing over
ServiceAddress value). This is an impure operation, but you can use
liftIO to make
Maybe Caravan value that can be passed to
checkCaravanCapacityOnError on the next line. This function returns
Either Error Reservation, but since this function is defined in an
EitherT Error IO Reservation context,
newRes is a
Reservation value. Still, it's important to realise that exactly because of this context, execution will short-circuit at that point if the return value from
checkCaravanCapacityOnError is a
Left value. In other words, all subsequent expression are only evaluated if
Right. This means that the
reserveCaravan function is only called if a caravan with enough capacity is available.
checkCaravan function will unconditionally execute if called, so as the final composition step, you'll need to figure out how to compose it into the overall
postReservation function in such a way that it's only called if
Conditional composition #
In the previous incarnation of this example, the overall entry point for the HTTP request in question was this
postReservation :: ReservationRendition -> IO (HttpResult ()) postReservation candidate = fmap toHttpResult $ runEitherT $ do r <- hoistEither $ validateReservation candidate i <- liftIO $ getReservedSeatsFromDB connStr $ date r hoistEither $ checkCapacity 10 i r >>= liftIO . saveReservation connStr
Is it possible to compose
checkCaravan into this function in such a way that it's only going to be executed if
Left? Yes, by adding to the
hoistEither $ checkCapacity 10 i r pipeline:
import Control.Monad.Trans (liftIO) import Control.Monad.Trans.Either (EitherT(..), hoistEither, right, eitherT) postReservation :: ReservationRendition -> IO (HttpResult ()) postReservation candidate = fmap toHttpResult $ runEitherT $ do r <- hoistEither $ validateReservation candidate i <- liftIO $ getReservedSeatsFromDB connStr $ date r eitherT (checkCaravan r) right $ hoistEither $ checkCapacity 10 i r >>= liftIO . saveReservation connStr
Contrary to F#, you have to read Haskell pipelines from right to left. In the second-to-last line of code, you can see that I've added
eitherT (checkCaravan r) right to the left of
hoistEither $ checkCapacity 10 i r, which was already there. This means that, instead of binding the result of
hoistEither $ checkCapacity 10 i r directly to the
saveReservation composition (via the monadic
>>= bind operator), that result is first passed to
eitherT (checkCaravan r) right.
eitherT function composes two other functions: the leftmost function is invoked if the input is
Left, and the right function is invoked if the input is
Right. In this particular example,
(checkCaravan r) is the closure being invoked in the
Left - and only in the
Left - case. In the
Right case, the value is passed on unmodified, but elevated back into the
EitherT context using the
(BTW, the above composition has a subtle bug: the capacity is still hard-coded as
10, even though reserving extra caravans actually increases the overall capacity of the restaurant for the day. I'll leave it as an exercise for you to make the capacity take into account any reserved caravans. You can download all of the source code, if you want to give it a try.)
Haskell has strong support for composition of functions. Not only can you interleave pure and impure code, but you can also do it conditionally. In the above example, the
eitherT function holds the key to that. The overall flow of the
postReservation function is:
- Validate the input
- Get already reserved seats from the database
- Check the reservation request against the restaurant's remaining capacity
- If the capacity is exceeded, attempt to reserve a caravan of sufficient capacity
- If one of the previous steps decided that the restaurant has enough capacity, then save the reservation in the database
- Convert the result (whether it's
Right) to an HTTP response
Haskell's type system is remarkably helpful here. Haskell programmers often joke that if it compiles, it works, and there's a morsel of truth in that sentiment. Both functions used with
eitherT must return a value of the same type, but the left function must be a function that takes the
Left type as input, whereas the right function must be a function that takes the
Right type as input. In the above example,
(checkCaravan r) is a partially applied function with the type
Error -> EitherT Error IO Reservation; that is: the input type is
Error, so it can only be composed with an
Either Error a value. That matches the return type of
checkCapacity 10 i r, so the code compiles, but if I accidentally switch the arguments to
eitherT, it doesn't compile.
I find it captivating to figure out how to 'click' together such interleaving functions using the composition functions that Haskell provides. Often, when the composition compiles, it works as intended.
This is something of a tangent, but I wanted to hint to you that Haskell can help reduce the boilerplate it takes to compose monadic computations like this.
MonadErrorclass abstracts monads which support throwing and catching errors. If you don't specify the concrete monad (transformer stack) a computation lives in, it's easier to compose it into a larger environment.
I'm programming to the interface, not the implementation, by using
Right. This allows me to dispense with calls to
hoistEitherwhen I come to call my function in the context of a bigger monad:
reserveCaravanonly declare a
checkCaravanneeds to do both IO and error handling. The type class system lets you declare the capabilities you need from your monad without specifying the monad in question. The elaborator figures out the right number of calls to
liftwhen it builds the
MonadErrordictionary, which is determined by the concrete type you choose for
mat the edge of your system.
A logical next step here would be to further constrain the effects that a given IO function can perform. In this example, I'd consider writing a separate class for monads which support calling the caravan service:
findCaravan :: MonadCaravanService m => ServiceAddress -> Int -> ZonedTime -> m (Maybe Caravan). This ensures that
findCaravancan only call the caravan service, and not perform any other IO. It also makes it easier to mock functions which call the caravan service by writing a fake instance of
F# doesn't support this style of programming because it lacks higher-kinded types. You can't abstract over
m; you have to pick a concrete monad up-front. This is bad for code reuse: if you need to add (for example) runtime configuration to your application you have to rewrite the implementation of your monad, and potentially every function which uses it, rather than just tacking on a
MonadReaderconstraint to the functions that need it and adding a call to
runReaderTat the entry point to your application.
Finally, monad transformers are but one style of effect typing;
extensible-effectsis an alternative which is gaining popularity.
Thank you very much for the elaborate explanation. I'm also delighted that you stuck with my admittedly contrived example of using caravans to compensate for the restaurant's lack of space. Or is that nothing compared to some of the stranger real-life feature requests some of us have seen?
I agree with your point on my previous comment that my suggestion could be considered a leaky abstraction and would introduce unnecessary requirements to the implementation. It just feels strange to let go of the idea that the domain logic is to be unconditionally pure, and not just mostly pure with the occasional impure function passed in as an argument. It's like what you say towards the end of the post - I feel hesitant to mix branching code and composition code together. The resulting solution you propose here is giving me second thoughts though. The
postReservationfunction didn't become much more complex as I'd feared, with the branching logic nicely delegated to the
eitherTfunction. The caravan logic also gets its own composition function that is easy enough to understand on its own. I guess I've got some thinking to do.
So, a final question regarding this example: To what extent would you apply this technique when solving the same problem in F#? It seems that we are using an increasing amount of Haskell language features not present in F#, so maybe not everything would translate over cleanly.
Martin, I'm still experimenting with how that influences my F# code. I'd like to at least attempt to back-port something like this to F# using computation expressions, but it may turn out that it's not worth the effort.