Lately I have been playing with this type, which I understand to be an encoding of the free distributive functor (for tangential background on that, see this answer):
data Ev g a where
Ev :: ((g x -> x) -> a) -> Ev g a
deriving instance Functor (Ev g)
The existential constructor ensures I can only consume an Ev g
by supplying a polymorphic extractor forall x. g x -> x
, and that the lift and lower functions of the free construction can be given compatible types:
runEv :: Ev g a -> (forall x. g x -> x) -> a
runEv (Ev s) f = s f
evert :: g a -> Ev g a
evert u = Ev $ \f -> f u
revert :: Distributive g => Ev g a -> g a
revert (Ev s) = s <$> distribute id
However, there is a difficulty upon trying to give Ev g
a Distributive
instance. Given that Ev g
is ultimately just a function with a weird argument type, one might hope that just threading distribute
for functions (which amounts to (??) :: Functor f => f (a -> b) -> a -> f b
from lens, and doesn't inspect the argument type in any way) through the Ev
wrapper:
instance Distributive (Ev g) where
distribute = Ev . distribute . fmap (\(Ev s) -> s)
That, however, does not work:
Flap3.hs:95:53: error:
• Couldn't match type ‘x’ with ‘x0’
‘x’ is a rigid type variable bound by
a pattern with constructor:
Ev :: forall (g :: * -> *) x a. ((g x -> x) -> a) -> Ev g a,
in a lambda abstraction
at Flap3.hs:95:44-47
Expected type: (g x0 -> x0) -> a
Actual type: (g x -> x) -> a
• In the expression: s
In the first argument of ‘fmap’, namely ‘(\ (Ev s) -> s)’
In the second argument of ‘(.)’, namely ‘fmap (\ (Ev s) -> s)’
• Relevant bindings include
s :: (g x -> x) -> a (bound at Flap3.hs:95:47)
|
95 | distribute = Ev . distribute . fmap (\(Ev s) -> s)
| ^
Failed, no modules loaded.
GHC objects to rewrapping the existential, even though we do nothing untoward with it between unwrapping and rewrapping. The only way out I found was resorting to unsafeCoerce
:
instance Distributive (Ev g) where
distribute = Ev . distribute . fmap (\(Ev s) -> unsafeCoerce s)
Or, spelling it in a perhaps more cautious manner:
instance Distributive (Ev g) where
distribute = eevee . distribute . fmap getEv
where
getEv :: Ev g a -> (g Any -> Any) -> a
getEv (Ev s) = unsafeCoerce s
eevee :: ((g Any -> Any) -> f a) -> Ev g (f a)
eevee s = Ev (unsafeCoerce s)
Is it possible to get around this problem without unsafeCoerce
? or there truly is no other way?
Additional remarks:
I believe
Ev
is the most correct type I can give to the construction, though I would be happy to be proved wrong. All my attempts to shift the quantifiers elsewhere lead either to needingunsafeCoerce
somewhere else or toevert
andrevert
having types that don't allow them to be composed.This situation looks, at first glance, similar to the one described in this blog post by Sandy Maguire, which ends up sticking with
unsafeCoerce
as well.
The following take at giving Ev g
a Representable
instance might put the problem into sharper relief. As dfeuer notes, this isn't really supposed to be possible; unsurprisingly, I had to use unsafeCoerce
again:
-- Cf. dfeuer's answer.
newtype Goop g = Goop { unGoop :: forall y. g y -> y }
instance Representable (Ev g) where
type Rep (Ev g) = Goop g
tabulate f = Ev $ \e -> f (Goop (goopify e))
where
goopify :: (g Any -> Any) -> g x -> x
goopify = unsafeCoerce
index (Ev s) = \(Goop e) -> s e
While goopify
sure looks alarming, I think there is a case for it being safe here. The existential encoding means any e
that gets passed to the wrapped function will necessarily be an extractor polymorphic on the element type, that gets specialised to Any
merely because I asked for that to happen. That being so, forall x. g x -> x
is a sensible type for e
. This dance of specialising to Any
only to promptly undo it with unsafeCoerce
is needed because GHC forces me to get rid of the existential by making a choice. This is what happens if I leave out the unsafeCoerce
in this case:
Flap4.hs:64:37: error:
• Couldn't match type ‘y’ with ‘x0’
‘y’ is a rigid type variable bound by
a type expected by the context:
forall y. g y -> y
at Flap4.hs:64:32-37
Expected type: g y -> y
Actual type: g x0 -> x0
• In the first argument of ‘Goop’, namely ‘e’
In the first argument of ‘f’, namely ‘(Goop e)’
In the expression: f (Goop e)
• Relevant bindings include
e :: g x0 -> x0 (bound at Flap4.hs:64:24)
|
64 | tabulate f = Ev $ \e -> f (Goop e)
| ^
Failed, no modules loaded.
Prolegomena needed to run the code here:
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
import Data.Distributive
import Data.Functor.Rep
import Unsafe.Coerce
import GHC.Exts (Any)
-- A tangible distributive, for the sake of testing.
data Duo a = Duo { fstDuo :: a, sndDuo :: a }
deriving (Show, Eq, Ord, Functor)
instance Distributive Duo where
distribute u = Duo (fstDuo <$> u) (sndDuo <$> u)
Ev g
by supplying a polymorphic extractor" Perhaps you could make that more explicit, likeEv :: ((forall x . g x -> x) -> a) -> Ev g a
. Such definition makes the definitiondistribute wrapped = Ev (\f -> fmap (\(Ev z) -> z f) wrapped)
compile for me, although it breaks the generic deriving ofFunctor
for some reason. – Conventiclerevert
from typechecking, asdistribute id :: g (g a -> a)
won't be polymorphic enough to map the wrapped function over it. – Azotemiafmap (\(Ev s) -> s)
really something sensible to do? The object you arefmap
-ing over could contain manyEv
(say,fmap = map
), each with its own existentially quantifiedx
. – IsoagglutininDistributive
isEv g a = (forall g'. Distributive g' => (forall x. g x -> g' x) -> g' a)
, but I don't think that helps in making yourEv
work. – Parousx
does not appear in a "strictly positive" position, so it seems that any such differences won't be observable. For instance[Ev (\e -> show . e $ Duo 1 2), Ev (\e -> e $ Duo "foo" "bar")]
typechecks asEv Duo String
and works just fine. (Note that I must straighten out the result types of the suspensions toString
.) – AzotemiaEv
:toEvFinal (Ev s) = EvFinal $ \n -> revert (Ev (s . (. n)))
andfromEvFinal (EvFinal s) = s evert
. – AzotemiaunsafeCoerce
inrevert
. This avoids usingdata
to wrap a function. As you say,distribute id :: forall x. g (g x -> x)
is insufficiently polymorphic, but you can do the same thing: specialize tox ~ Any
and coerce under thefmap
. – Faenzax
es are behind a function arrow, so the secretly wrapped values aren't really ofx
type; they merely dictate a specialisation of a polymorphicx
. The customary trick is to encode existential quantification with universal quantification. If I understand it correctly, I have done it twice here, which would make it become universal again. – AzotemiaunsafeCoerce
is necessary, assuming there really is no way to get rid of it altogether. Auspiciously, neither theDistributive
instance nor theRepresentable
one requires a furtherunsafeCoerce
. – Azotemia