haskell - Advice on writing monadic signatures -
consider following example functions both add random value pure input:
addrand1 :: (monadrandom m) => m (int -> int) addrand2 :: (monadrandom m) => int -> m int -- *can* written m (int -> int)
it easy convert addrand1
function same signature addrand2
, but not vice versa.
to me, provides strong evidence should write addrand1
on addrand2
. in example, addrand1
has more truthful/general type, typically captures important abstractions in haskell.
while having "right" signature seems crucial aspect of functional programming, have lot of practical reasons why addrand2
might better signature, if could written addrand1
s signature.
with interfaces:
class fakemonadrandom m getrandom :: (random a, num a) => m getrandomr1 :: (random a, num a) => (a,a) -> m getrandomr2 :: (random a, num a) => m ((a,a) -> a)
suddenly
getrandomr1
seems "more general" in sense permits more instances (that repeatedly callgetrandom
until result in range, example) comparedgetrandomr2
, seems require sort of reduction technique.addrand2
easier write/read:addrand1 :: (monadrandom m) => m (int -> int) addrand1 = x <- getrandom return (+x) -- in general requires `return $ \a -> ...` addrand2 :: (monadrandom m) => int -> m int addrand2 = (a+) <$> getrandom
addrand2
easier use:foo :: (monadrandom m) => m int foo = x <- addrand1 <*> (pure 3) -- ugly syntax f <- addrand1 -- or 2 step process: sequence function, apply x' <- addrand2 3 -- easy! return $ (f 3) + x + x'
addrand2
harder misuse: considergetrandomr :: (monadrandom m, random a) => (a,a) -> m a
. given range, can sample repeatedly , different results, intend. however, if instead havegetrandomr :: (monadrandom m, random a) => m ((a,a) -> a)
, might tempted writedo f <- getrandomr return $ replicate 20 $ f (-10,10)
but surprised result!
i'm feeling conflicted how write monadic code. "version 2" seems better in many cases, came across example "version 1" signature required.*
what sort of factors should influence design decisions w.r.t. monadic signatures? there way reconcile apparently conflicting goals of "general signatures" , "natural, clean, easy-to-use, hard-to-misuse syntax"?
*: wrote function foo :: -> m b
, worked fine (literally) many years. when tried incorporate new application (dsl hoas), discovered unable to, until realized foo
rewritten have signature m (a -> b)
. new application possible.
this depends on multiple factors:
- what signatures possible (here both).
- what signatures convenient use.
- or more generally, if want have general interface or dually general implementations.
the key understanding difference between int -> m int
, m (int -> int)
in former case, effect (m ...
) can depend on input argument. example, if m
io
, have function launches n
missiles, n
function argument. on other hand, effect in m (int -> int)
doesn't depend on - effect doesn't "see" argument of function returns.
coming case: take pure input, generate random number , add input. can see effect (generating random number) doesn't depend on input. why can have signature m (int -> int)
. if task instead generate n
random numbers, example, signature int -> m [int]
work, m (int -> [int])
wouldn't.
regarding usability, int -> m int
more common in monadic context, because monadic combinators expect signatures of form a -> b -> ... -> m r
. example, you'd write
getrandom >>= addrand2
or
addrand2 =<< getrandom
to add random number random number.
signatures m (int -> int)
less common monads, used applicative functors (each monad applicative functor), effects can't depend on parameters. in particular, operator <*>
work nicely here:
addrand1 <*> getrandom
regarding generality, signature influences how difficult use or implement it. observed, addrand1
more general caller's perspective - can convert addrand2
if needed. on other hand, addrand2
less general, therefore easier implement. in case doesn't apply, in cases might happen it's possible implement signature m (int -> int)
, not int -> m int
. reflected in type hierarchy - monads more specific applicative functors, means give more power user, "harder" implement - every monad applicative, not every applicative can made monad.
Comments
Post a Comment