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
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.