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 addRand1
s 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
getRandomR1
seems "more general" in the sense that it permits more instances (that repeatedly callgetRandom
until the result is in the range, for example) compared togetRandomR2
, which seems to require some sort of reduction technique.addRand2
is 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+) <$> getRandom
addRand2
is 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'
addRand2
is 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.