8

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'''?

amalloy
  • 89,153
  • 8
  • 140
  • 205

1 Answers1

5

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.

Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56
  • But why can't it be inferred? I agree that `coerce liftA3 Vec3 getX getY getZ` isn't enough information, but I expected the type information to flow the other way as well: the type at which I'm trying to use `coerce` is constrained by the fact that I've declared that `getCompose go` has type `Input -> Maybe Vec3`. – amalloy Dec 07 '21 at 14:20
  • 2
    @amalloy In `liftA3`, there are multiple `f`s that are constrained to be equal to each other, so information can flow from the "output" `f` to the inputs. But `coerce liftA3` doesn't share that property -- maybe you wanted different choices of `newtype` than `Compose` for those! – Daniel Wagner Dec 07 '21 at 15:07
  • @amalloy Concretely, if `F a` also shares a representation with `Input -> Maybe a`, then `liftA3 :: (Int -> Int -> Int -> Vec3) -> F Int -> F Int -> F Int -> F Vec3` can *also* be `coerce`d to the type required in the spot it's used. – Daniel Wagner Dec 07 '21 at 15:13
  • @DanielWagner Right, I think that clears it up. I was "wishing" that GHC would know I only wanted to coerce input arguments to `liftA3`, and that my desired output type would guide inference on what I want to coerce to. But sadly, `coerce` is more powerful: I might have intended to coerce the inputs to some other `f` type, apply `liftA3`, and then coerce its output to match the expected `Compose` signature. – amalloy Dec 07 '21 at 15:16
  • @amalloy For what it's worth, I find this to be a general principle with `coerce`. It never *quite* gets nailed down how I wanted it to. I've tried several times to work out a general principle describing what I want and, beyond some vague hand gesturing, have never really gotten anywhere. – Daniel Wagner Dec 07 '21 at 15:20
  • This code snippet doesn't compile here. If I'm reading the type error correctly, I believe it's because which `Int` newtype wrapper `liftA3` is supposed to use isn't specified yet. Yikers. (Presumably a similar situation will come up for the `Vec3` part as well.) Would be nice for GHC to be willing to do "defaulting" or something like it when the `Coercible` constraint is on an otherwise parametrically polymorphic type... – Daniel Wagner Dec 07 '21 at 15:32
  • @DanielWagner In some cases, a way to limit the generality of `coerce` might be to write a type family which relates input and output types. – danidiaz Dec 07 '21 at 17:28
  • I think the problem is that in `getCompose (coerce liftA3 ...)` you *could* be coercing the `liftA3` that works on `Compose` to accept `Input -> Maybe Vec3` instead (and return a `Compose` as normal, for the `getCompose` to unpack). But you might also be coercing an entirely different `liftA3` (say, the one that goes with the `IO` Applicative), and thus *also* be coercing the return value to `Compose`. Either of those are just different choices for type variables in `coerce :: Coercible a b => a -> b`; neither is a priori preferred, only once we go actually looking for `Coercible` instances. – Ben Dec 08 '21 at 08:22
  • And as we know, GHC will **not** examine all of the possible choices for type variables to see if there is only one possibility that has a matching instance. It wants a polymorphic instance that matches the variables, or it wants the variables to already be instantiated by something else. – Ben Dec 08 '21 at 08:24