3

I was working on the following small fragment of code:

import           Control.Monad
import           Data.Aeson
import qualified Data.HashMap.Strict as HashMap
import           Data.Map (Map)
import qualified Data.Map as Map
import           GHC.Generics

-- definitions of Whitelisted, WhitelistComment and their FromJSON instances
-- omitted for brevity

data Whitelist = Whitelist
  { whitelist :: Map Whitelisted WhitelistComment
  } deriving (Eq, Ord, Show)

instance FromJSON Whitelist where
  parseJSON (Object v) =
    fmap (Whitelist . Map.fromList) . forM (HashMap.toList v) $ \(a, b) -> do
      a' <- parseJSON (String a)
      b' <- parseJSON b
      return (a', b')
  parseJSON _ = mzero

when I realised that I can rewrite the do block in applicative style:

instance FromJSON Whitelist where
  parseJSON (Object v) =
    fmap (Whitelist . Map.fromList) . forM (HashMap.toList v) $ \(a, b) ->
      (,) <$> parseJSON (String a) <*> parseJSON b
  parseJSON _ = mzero

and with that I could also replace forM with for. Before making the change above I switched to for first:

instance FromJSON Whitelist where
  parseJSON (Object v) =
    fmap (Whitelist . Map.fromList) . for (HashMap.toList v) $ \(a, b) -> do
      a' <- parseJSON (String a)
      b' <- parseJSON b
      return (a', b')
  parseJSON _ = mzero

and to my surprise this still compiled. Given the definition of for:

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

I thought the Applicative constraint would prevent me from using do notation / return in the action passed to for.

I'm clearly missing something fundamental here, either in terms of what for signature really implies, or how the code I posted is interpreted by the compiler, and would appreciate any help understanding what's going on.

duplode
  • 33,731
  • 7
  • 79
  • 150
ppb
  • 905
  • 6
  • 9
  • 2
    Are you missing that [`Applicative` is a superclass of `Monad`](http://stackoverflow.com/questions/24112786/why-should-applicative-be-a-superclass-of-monad)? `forM`, `liftM`, etc. are reminders from when `Applicative` wasn't (in the Prelude) a superclass of `Monad`. – Alec Dec 14 '16 at 19:16
  • @Alec isn't that the other way around? Meaning that I could pass the code in applicative style into forM without any changes, but not in the opposite direction? I'm not quite sure how it would be possible to use `return` when `Applicative` constraint is present. – ppb Dec 14 '16 at 19:24
  • 2
    `parseJSON` works in the `Parser` monad, which is also an applicative (of course). `return` (and do notation) checks `Parser` is a monad -- it is. `for` checks it is also an applicative -- it is. Everything works just fine. – chi Dec 14 '16 at 19:29

2 Answers2

6

This is just the usual caller-vs-implementer duality going on, where one side gets flexibility and the other restriction.

for provides you with this interface:

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

You as the caller get the flexibility to choose any type f to instantiate it, so you can use it as if it were:

for :: Traversable t => t a -> (a -> Parser b) -> Parser (t b)

Clearly once you've done that, there's no reason you couldn't use any Parser-specific functionality in the function you pass to for, including Monad stuff.

The implementer of for on the other hand gets restricted by the polymorphism in the interface of for. They have to work with any choice of f, so they can only use the Applicative interface in the code they write to implement for. But that only restricts the code of for itself, not the function passed into it.

If the author of for wanted to restrict what the caller could do in that function, they could have used RankNTypes to instead provide this interface:

for :: forall t f. (Traversable t, Applicative f) => t a -> (forall g. Applicative g => a -> g b) -> f (t b)

Now the provided lambda itself must be polymorphic in g (subject to an Applicative constraint). The caller of for still gets the flexibility to choose f, with the implementer being restricted in using only Applicative features. But the caller of for is the implementer of the function argument, so now that that function is polymorphic itself the caller of for is restricted to using only Applicative features there and the implementer of for gets the freedom to use it with any type they like (including possibly using monad features to combine it with other internal values). With this specific type signature the implementer of for will have to choose to instantiate g with the same type the caller of for selected for f, in order to come up with the final f (t b) return value. But the caller of for would still be restricted by the type system to providing a function that works for any Applicative g.

The point is, if you get to choose what type to instantiate a polymorphic signature with then you are not the one restricted by that interface. You can choose a type and then use whatever other features of that type you like, provided you still do provide the bits of information the interface requires of you. i.e. You can use non-Traversable functionality to create your t a and non-Applicative functionality to create your a -> f b, all that's required is that you do provide those inputs. And indeed you almost have to make use of functionality specific to a and b. The implementer of the polymorphic signature doesn't get that freedom, they are restricted by the polymorphism to only doing things that would work for any possible choice.

As an aside, similarly to how rank 2 types add "another level" of this duality with the roles reversed (and rank N types allow arbitrarily many levels), a similar duality is also seen (flipped around again) in the constraints themselves. Consider again the signature:

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

The caller of for is restricted by the Traversable and Applicative constraints when choosing the types t and f. The implementer gets the freedom to use any functions implied by those constraints, without worrying about how to prove the constraints are satisfied.

Cirdec
  • 24,019
  • 2
  • 50
  • 100
Ben
  • 68,572
  • 20
  • 126
  • 174
5

The first short answer is that Parser has an Applicative instance. The snippet

do
  a' <- parseJSON a
  b' <- parseJSON b
  return (a', b')

has the type Parser (Whitelisted, WhitelistComment) which unifies with f b in the type signature

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

Since there's an Applicative Parser instance, it also satisfies that constraint. (I think I got the types for a' and b' right)


The second short answer is that a Monad is strictly more powerful than an Applicative, anywhere you need an Applicative you can use a Monad instead. Ever since the Monad-Applicative proposal was implemented, every Monad is also Applicative. The Monad class now looks like

class Applicative m => Monad m where 
    ...

A Monad is strictly more powerful than an Applicative, anywhere you need an Applicative you can use a Monad instead with the following substitutions:

  • ap instead of <*>
  • return instead of pure
  • liftM instead of fmap

If you're writing some new type, SomeMonad, and have provided an instance for the Monad class you can use it to provide the instances for Applicative and Functor too.

import Control.Monad

instance Applicative SomeMonad where
    pure = return
    (<*>) = ap

instance Functor SomeMonad where
    fmap = liftM
Cirdec
  • 24,019
  • 2
  • 50
  • 100
  • Thank you. I see how `Monad` is more powerful and that with AMP every `Monad` is an `Applicative` as well. But when a signature says it specifically wants an `Applicative` wouldn't that mean that it wants to restrict those powers and only work with `Applicative` interface? – ppb Dec 14 '16 at 19:58
  • 3
    Yes, `for` is only permitted to use the `Applicative` powers of the object you pass in. But you can use monadic powers to construct values of that type - how you do stuff inside your lambda is a black box to `for`. – amalloy Dec 14 '16 at 20:11
  • 1
    @amalloy I think I got it now - `for` doesn't care how the value was produced, only that it satisfies the `Applicative` constraint. I thought that the `Applicative` restriction would be imposed on what the lambda itself can do, not just on what it returns. – ppb Dec 14 '16 at 20:43
  • And it seems the crucial thing I missed was *producing* an `Applicative` vs *working* with one :-( – ppb Dec 14 '16 at 20:54
  • 2
    @ppb It's not about _producing_ vs _working_ with a type. It's about what you (and the type system) know about the type. When you call `for :: forall f. ...` you are passing `for` both its arguments and the type `f`. You know more about `f` than it's an `Applicative f` or a `Monad f`; you know what `f` is! It's `Parser`. You can use anything you know about the type, including crazy things that aren't even a part of either `Applicative` or `Monad`, like `parseJSON` (which you're already using). If you know that `f` is `Parser` to use `parseJSON`, you also know it's a `Monad` to use `>>=`. – Cirdec Dec 14 '16 at 21:08