Search code examples
haskellmonadscategory-theory

What is an generalized way to compose these 3 types of functions for conditional branching?


I have three monadic functions which I want to compose together and conditionally branch on a predicate. I'm looking for possibly multiple general solutions with tradeoffs. Arrows (ArrowChoice?) and Monads are looking promising.

The contrived problem is this:

I run a service that keeps track of many clients' "personal number". If they log in for the first time, their number is set at 0. If they've logged in before, and presumably changed their number, that is the number that will be fetched for them.

So the generic type of this program could be program :: Name -> Int.

I have 3 functions, which all might have a monadic effect of querying a persistent store of data (eg DB, State):

new?     :: Name -> Bool
fetch    :: Name -> Int
generate :: Name -> Int

I want to be able to swap these functions out for others. For example, swap out fetch for fetchFromDB, or fetchFromStateMonad.

One solution for "composing" them could look like this:

ex1 :: (a -> Bool) -> (a -> b) -> (a -> b) -> (a -> b)
-- or --
ex1 :: (Name -> Bool) -> (Name -> Int) -> (Name -> Int) -> (Name -> Int)
--     predicate         fetch            generate         result

This looks a bit like ifM which gets me closer to a generic solution.

But when I call ex1 "John", it'll make one database call for determining the predicate, and an other to fetch John's number. (of course, this is easy to solve in this narrow case. I'm looking for a more general one).

ex2 :: Name -> (Name -> Bool) -> (Name -> Int) -> (Name -> Int) -> Int
--     name    predicate         fetch            generate      result         

It's still not clear to me how to thread one single database query result through the predicate and fetch, but at least this type signature could allow for it. Now there's the issue of a type signature though which is less like composition, and more ad hoc.

Is there a way to generalize this problem? Where some composition function does everything necessary to allow arbitrary predicates, and functions, to wire themselves up in the way I intended.


Solution

  • If new? and fetch are separate functions, you will have to make two queries in any case. (One to check if the name is in the database, then another after you examine the result.)

    However, it seems like new? is redundant; fetch could return a Maybe value to either retrieve the desire number or indicate that the name is new. Also assuming that your monad is an instance of MonadIO, your signature and function would be something like

    program :: MonadIO m => Name                  -- name to query
                         -> (Name -> m Maybe Int) -- fetch
                         -> (Name -> m Int)       -- generate
                         -> m Int                 -- number for name
    program name fetch generate = fetch name >>= maybe (generate name) return