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 coerce
s 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'''
?
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.