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