4

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.

  1. 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 call getRandom until the result is in the range, for example) compared to getRandomR2, which seems to require some sort of reduction technique.

  2. 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
    
  3. 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'
    
  4. addRand2 is harder to misuse: consider getRandomR :: (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 have getRandomR :: (MonadRandom m, Random a) => m ((a,a) -> a), we might be tempted to write

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

Community
  • 1
  • 1
crockeea
  • 21,651
  • 10
  • 48
  • 101
  • 2
    Those two signatures are truly semantically different. Are you selecting an action to perform based on input, or are you doing something like reading a translations file and returning a function that does the translation? `a -> m b` is definitely used much more than the other one. – Lazersmoke Apr 07 '17 at 14:21
  • @Lazersmoke I gave definitions for both. You can see that the monadic action does not depend on the input in `addRand2`; it behaves just like `addRand1`. My feeling is that `a -> m b` is used more frequently as well; I'm trying to understand why that's the case. – crockeea Apr 07 '17 at 14:29
  • 2
    Your function `addRand1` is basically just using the Functor instance that is a constraint of every Monad. You could rewrite it as: `addRand1 = fmap (+) getRandom`. Since `getRandom` needs to be in a Monad context anyway and not just Functor, I don't think it is any less powerful to write your function in the second way. If you want to go pointfree style with `addRand2` just for the sake of it, you could do: `addRand2 = liftM2 (+) getRandom . return` but your version is much more readable :-) – basile-henry Apr 07 '17 at 15:06
  • @Boomerang "Since getRandom needs to be in a Monad context anyway and not just Functor, I don't think it is any less powerful to write your function in the second way." Could you explain that a bit more? I gave an example in the note that explains why I required the "first way" signature. – crockeea Apr 07 '17 at 15:09
  • Well I wrote that sentence in case you wanted to make the signature more general `Applicative f => f (Int -> Int)`. But a Monad context is required by `getRandom`. It wasn't really well phrased, sorry if I confused you. I think the key difference between the two is when you use `addRand1` you only perform the IO action once and therefore end up using the same random number when you apply the function you get consecutively. `addRand2` seems more useful. I'm not familiar enough with HOAS, but if you need to keep track of a specific environment (your random number) then `addRand1` is approriate – basile-henry Apr 07 '17 at 15:25
  • What's _more powerful_ is distinct from _what's actually doing what you mean._ `addRand1` claims that inside the `MonadRandom` monad you can create a _pure_ function that always has the same output for the same input. That isn't how randomness _works._ The signature is more general because it places more limitations on you -- limitations that are incompatible with any notion of randomness. – Louis Wasserman Apr 07 '17 at 21:57
  • @LouisWasserman Both `addRand1` and `addRand2` do what I mean: they both add a random value to an input, when called exactly one time. Of course there's a difference when called repeatedly, which is basically my point (4). I don't understand what's "incompatible with any notion of randomness". – crockeea Apr 07 '17 at 22:03
  • A randomness function usable "when called exactly one time" is useless. The whole point of randomness is that you use it more than once, otherwise it's https://xkcd.com/221/ – Louis Wasserman Apr 07 '17 at 22:04
  • I see no difference between applying a function to an input and sequencing the result multiple times, vs sequencing a function multiple times and applying it to the same input. Of course you have to use it correctly. See bullet (4). – crockeea Apr 07 '17 at 23:01
  • 2
    FWIW, `(<*>) :: Applicative m => m (a -> b) -> m a -> m b`, and `(=<<) :: Monad m => (a -> m b) -> m a -> m b`, so you could argue one is the "Applicative way" and the other is the "Monadic way." – Lazersmoke Apr 08 '17 at 00:53

4 Answers4

5

This depends on multiple factors:

  • What signatures are actually possible (here both).
  • What signatures are convenient to use.
  • Or more generally, if you want to have the most general interface or dually the most general implementations.

The key for understanding the difference between Int -> m Int and m (Int -> Int) is that in the former case, the effect (m ...) can depend on the input argument. For example, if m is IO, you could have a function that launches n missiles, where n is the function argument. On the other hand, the effect in m (Int -> Int) doesn't depend on anything - the effect doesn't "see" the argument of the function it returns.

Coming back you your case: You take a pure input, generate a random number and add it to the input. We can see that the effect (generating the random number) doesn't depend on the input. This is why we can have signature m (Int -> Int). If the task were instead to generate n random numbers, for example, signature Int -> m [Int] would work, but m (Int -> [Int]) wouldn't.

Regarding usability, Int -> m Int is more common in the monadic context, because most monadic combinators expect signatures of the form a -> b -> ... -> m r. For example, you'd normally write

getRandom >>= addRand2

or

addRand2 =<< getRandom

to add a random number to another random number.

Signatures like m (Int -> Int) are less common for monads, but often used with applicative functors (each monad is also an applicative functor), where effects can't depend on parameters. In particular, operator <*> would work nicely here:

addRand1 <*> getRandom

Regarding generality, the signature influences how difficult is to use or to implement it. As you observed, addRand1 is more general from the caller's perspective - it can always convert it to addRand2 if needed. On the other hand, addRand2 is less general, therefore easier to implement. In your case it doesn't really apply, but in some cases it might happen that it's possible to implement signature like m (Int -> Int), but not Int -> m Int. This is reflected in the type hierarchy - monads are more specific then applicative functors, which means they give more power to their user, but are "harder" to implement - every monad is an applicative, but not every applicative can be made into a monad.

Petr
  • 62,528
  • 13
  • 153
  • 317
2

It is easy to convert addRand1 into a function with the same signature as addRand2, but not vice versa.

Ahem.

-- | Adds a random value to its input
addRand2 :: MonadRandom m => Int -> m Int
addRand2 x = fmap (+x) getRand

-- | Returns a function which adds a (randomly chosen) fixed value to its input
addRand1 :: MonadRandom m => m (Int -> Int)
addRand1 = fmap (+) (addRand2 0)

How come this works? Well, addRand1's job is to come up with a randomly chosen value and partially apply + to it. Adding a random number to a dummy value is a perfectly good way of coming up with a random number!

I think you might be confused about the quantifiers in @chi's statement. He didn't say

For all monads m and types a and b, you can't convert a -> m b to m (a -> b)

∀ m a b. ¬ ∃ f. f :: Monad m => (a -> m b) -> m (a -> b)

He said

You can't convert a -> m b to m (a -> b) for all monads m and types a and b.

¬ ∃ f. f :: ∀ m a b. Monad m => (a -> m b) -> m (a -> b)

Nothing stops you from writing (a -> m b) -> m (a -> b) for a particular monad m and pair of types a and b.

Community
  • 1
  • 1
Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • "Ahem." Fine, but you cheated. You're using the definition of the function to come up with something that does an equivalent action. I'm saying exactly what `chi` said. I don't give a whit about `addRand*`; they're just examples to demonstrate the problem. – crockeea Apr 07 '17 at 16:15
  • 2
    I'll be less obtuse. Any time you can write an `x :: a -> m b` and `y :: m (a -> b)` (for some `m`, `a`, `b`), such that `x = liftAp y` per your question (`liftAp :: Functor f => f (a -> b) -> a -> f b`), you're gonna be doing it in at least a type-specific way and usually a meaning-specific way, because there is no general way. I mean to say: for most monads and specifications of `x`/`y` you can't write both `x` and `y`. If your question isn't about the code in your question then you increase the likelihood of getting answers about the wrong thing – Benjamin Hodgson Apr 07 '17 at 17:02
  • The point of giving specific code was to demonstrate how `addRand1` is harder to write and use. The question is just about the tradeoffs between the two styles of signatures for functions that can be written either way. – crockeea Apr 07 '17 at 17:05
1

Why not both?

Or, in English: why not both? It is rare that both signatures are possible, but when they are, each version can be useful in different contexts.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • Fair, but it could still be problematic if the `m (a->b)` version gets misused as in point (4). – crockeea Apr 07 '17 at 16:16
  • 1
    @crockeea I think most people are quite familiar with the difference between `replicate` and `replicateM` (and the other `foo` vs `fooM` functions). I'm not at all worried about point 4. – Daniel Wagner Apr 07 '17 at 16:31
0

It is easy to convert addRand1 into a function with the same signature as addRand2, but not vice versa.

This is true, but don't forget that this "conversion" does not have to preserve the intended semantics.

I mean, if we have foo :: IO (Int -> ()) we can write

bogusPrint :: Int -> IO ()
bogusPrint x = ($ x) <$> foo

but this will perform the same IO action for all x! Hardly useful.

Your argument seems to be "I can define an object x :: A or another y :: B. Well, I also know I can write f :: A->B, so x :: A is more general since y can let y = f x :: B". Personally, I think this is a great approach to reason about your code! However, one must check that the y obtained as f x is the intended one. Just because types match, it doesn't imply that the value is correct.

So, in the general case, I think it depends on the monad at hand. I'd write both x and y (as Daniel Wagner suggests), and then check whether one is really more general than the other -- not just because the type is more general, but because the value y can be (efficiently) recovered from x.

chi
  • 111,837
  • 3
  • 133
  • 218
  • "This will perform the same `IO` action for all `x`!" So does `foo` though? Not sure I follow your argument – Benjamin Hodgson Apr 07 '17 at 21:21
  • @BenjaminHodgson My point is, if you want a meaningful `realPrint :: Int -> IO ()`, you can't say "let's instead define a more general `foo :: IO (Int -> ())`, and then derive `realPrint` out of it". Just because you can derive something of the same type, it does not mean that it is the intended value. Or perhaps, more informally "more general type does not imply more general value" – chi Apr 07 '17 at 21:43
  • I think I see what you mean. To put it maybe a bit more bluntly `id :: a -> a` doesn't generalise `succ :: Int -> Int` even though it has a more general type – Benjamin Hodgson Apr 07 '17 at 21:47
  • I do understand there's a difference between the types, unfortunately I lack the language to express that clearly. My function `foo :: m (a -> b)` "does the same thing" as my `bar :: a -> m b` function, namely because the randomness on my *particular* function doesn't depend on value of the input `a`. Put another way, I could define `bar a = ($ a) <$> foo`. The question wasn't supposed to be about my particular code, but a general one about why I should choose one signature over the other when I have a function with the type of `foo`. – crockeea Apr 07 '17 at 21:56