Consider the following example functions which both add a random value to the pure input:
addRand1 :: (MonadRandom m) => m (Int -> Int)
addRand2 :: (MonadRandom m) => Int -> m Int -- *can* be written as m (Int -> Int)
It is easy to convert addRand1 into a function with the same signature as addRand2, but not vice versa.
To me, this provides strong evidence that I should write addRand1 over addRand2. In this example, addRand1 has the more truthful/general type, which typically captures important abstractions in Haskell.
While having the "right" signature seems a crucial aspect of functional programming, I also have a lot of practical reasons why addRand2 might be a better signature, even if it could be written with addRand1s signature.
With interfaces:
class FakeMonadRandom m where getRandom :: (Random a, Num a) => m a getRandomR1 :: (Random a, Num a) => (a,a) -> m a getRandomR2 :: (Random a, Num a) => m ((a,a) -> a)Suddenly
getRandomR1seems "more general" in the sense that it permits more instances (that repeatedly callgetRandomuntil the result is in the range, for example) compared togetRandomR2, which seems to require some sort of reduction technique.addRand2is easier to write/read:addRand1 :: (MonadRandom m) => m (Int -> Int) addRand1 = do x <- getRandom return (+x) -- in general requires `return $ \a -> ...` addRand2 :: (MonadRandom m) => Int -> m Int addRand2 a = (a+) <$> getRandomaddRand2is easier to use:foo :: (MonadRandom m) => m Int foo = do x <- addRand1 <*> (pure 3) -- ugly syntax f <- addRand1 -- or a two step process: sequence the function, then apply it x' <- addRand2 3 -- easy! return $ (f 3) + x + x'addRand2is harder to misuse: considergetRandomR :: (MonadRandom m, Random a) => (a,a) -> m a. For a given range, we can sample repeatedly and get different results, which is probably what we intend. However, if we instead havegetRandomR :: (MonadRandom m, Random a) => m ((a,a) -> a), we might be tempted to writedo f <- getRandomR return $ replicate 20 $ f (-10,10)but will be very surprised by the result!
I'm feeling very conflicted about how to write monadic code. The "version 2" seems better in many cases, but I recently came across an example where the "version 1" signature was required.*
What sort of factors should influence my design decisions w.r.t. monadic signatures? Is there some way to reconcile apparently conflicting goals of "general signatures" and "natural, clean, easy-to-use, hard-to-misuse syntax"?
*: I wrote a function foo :: a -> m b, which worked just fine for (literally) many years. When I tried to incorporate this into a new application (DSL with HOAS), I discovered I was unable to, until I realized that foo could be rewritten to have the signature m (a -> b). Suddenly my new application was possible.
