haskellapplicative

Why doesn't `coerce` implicitly apply Compose to these functions?


Consider this type:

data Vec3 = Vec3 {_x, _y, _z :: Int}

I have some functions that all take the same input, and may fail to compute a field:

data Input
getX, getY, getZ :: Input -> Maybe Int

I can write a function that tries all three of these field constructors:

v3 :: Input -> Maybe Vec3
v3 i = liftA3 Vec3 (getX i) (getY i) (getZ i)

But it's kinda annoying to have to pass that i input around three times. Functions are themselves an applicative functor, and so one can replace foo x = bar (f x) (g x) (h x) with foo = liftA3 bar f g h.

Here, my bar is liftA3 Vec3, so I could write

v3' :: Input -> Maybe Vec3
v3' = liftA3 (liftA3 Vec3) getX getY getZ

But this is a bit gross, and when we work with composed applicatives in this way (((->) Input) and Maybe), there's the Compose newtype to handle this kind of thing. With it, I can write

v3'' :: Input -> Maybe Vec3
v3'' = getCompose go
  where go = liftA3 Vec3 x y z
        x = Compose getX
        y = Compose getY
        z = Compose getZ

Okay, not exactly a great character savings, but we're now working with one combined functor instead of two, which is nice. And I thought I could use coerce to win me back some of the characters: after all, x is just a newtype wrapper around getX, and likewise for the other fields. So I thought I could coerce liftA3 into accepting three Input -> Maybe Vec3 instead of accepting three Compose ((->) Input) Maybe Vec3:

v3''' :: Input -> Maybe Vec3
v3''' = getCompose go
  where go = coerce liftA3 Vec3 getX getY getZ

But this doesn't work, yielding the error message:

tmp.hs:23:14: error:
    • Couldn't match representation of type ‘f0 c0’
                               with that of ‘Input -> Maybe Int’
        arising from a use of ‘coerce’
    • In the expression: coerce liftA3 Vec3 getX getY getZ
      In an equation for ‘go’: go = coerce liftA3 Vec3 getX getY getZ
      In an equation for ‘v3'''’:
          v3'''
            = getCompose go
            where
                go = coerce liftA3 Vec3 getX getY getZ
   |
23 |   where go = coerce liftA3 Vec3 getX getY getZ
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I don't understand why not. I can write coerce getX :: Compose ((->) Input) Maybe Int, and this is fine. And generally a function can be coerced in order to make it coerce its arguments or return type, as in

coerce ((+) :: Int -> Int -> Int) (Max (5::Int)) (Min (8::Int)) :: Sum Int

And I can in fact write out all the coerces individually:

v3'''' :: Input -> Maybe Vec3
v3'''' = getCompose go
  where go = liftA3 Vec3 x y z
        x = coerce getX
        y = coerce getY
        z = coerce getZ

So why can't liftA3 itself be coerced to accept getX instead of coerce getX, allowing me to use v3'''?


Solution

  • If you provide the applicative functor to liftA3, then the following typechecks:

    v3' :: Input -> Maybe Vec3
    v3' = coerce (liftA3 @(Compose ((->) Input) Maybe) Vec3) getX getY getZ
    

    In coerce liftA3 without any annotation, there is no way to infer what applicative functor to use liftA3 with. Neither of these even mention the type Compose. It might just as well be ReaderT Input Maybe, Kleisli Maybe Input, another type with an unlawful instance or something even more exotic.

    In getCompose (coerce liftA3 _ _ _) (your last attempt), the getCompose does not constraint liftA3 ("inside" of coerce), because getCompose is "outside" of coerce. It requires that the result type of liftA3 is coercible to Compose ((->) Input) Maybe Vec3, but it might still not be equal to that.