Recently I implemented a type hierarchy similar to Exception. The gist of it is this:
class (Typeable a) => Hierarchy a where
toHierarchy :: a -> HierarchyBase
fromHierarchy :: HierarchyBase -> Maybe a
-- HierarchyBase
data HierarchyBase = forall h. (Hierarchy h) => HierarchyBase h
instance Hierarchy HierarchyBase where
toHierarchy = id
fromHierarchy = Just
-- MyType ⊂ HierarchyBase
data MyType = forall e. (Hierarchy h) => MyType h
instance Hierarchy MyType where
toHierarchy x = HierarchyBase x
fromHierarchy (HierarchyBase x) = cast x
-- SubType ⊂ MyType ⊂ HierarchyBase
data SubType = SubType
instance Hierarchy SubType where
toHierarchy x = toHierarchy $ MyType x
fromHierarchy (HierarchyBase x) = cast x >>= \(MyType x') -> cast x'
-- dispatchHierarchy automatically casts a type into any of its ancestors.
-- for instance, you can do any of these:
-- > disptachHierarchy (f :: HierarchyBase -> ...) SubType
-- > disptachHierarchy (f :: MyType -> ...) SubType
-- > disptachHierarchy (f :: SubType -> ...) SubType
hierarchyDispatch :: (Hierarchy a, Hierarchy a') => (a' -> b) -> a -> b
hierarchyDispatch f h = f . fromJust . fromHierarchy . toHierarchy $ h
This solution uses existentially quantified data constructors to define some subtypeable hierarchy types. I've tried to rewrite it without using existentials, but I didn't manage.
I understand that using existential quantifiers this way lets you hide the type variable used in a type constructor: the datatype doesn't depend on it, and I can see why that's necessary to define the Hierarchy typeclass and its instances.
However I don't understand when or why this feature is useful or necessary. Now, every time I get stuck on a type's kind when defining a typeclass or an instance, I feel that I have to try to use existential quantifiers. But it never worked so far. The problem was always somewhere else. And my code is cluttered with unnecessary existentials (think of the classical example of having [Showable 1, Showable 'a'] instead of simply [show 1, show 'a']).
How can I tell when existentially quantified data constructors are needed or useful, as opposed to unnecessary overhead?
The simplification of existential datatypes to non-existentials follows from parametricity: the inability to pattern match on types.
But
Typeableis much harder to deal with thanShow. This is understandable:Typeable aessentially means "you can match the typeaagainst patterns". Even if you give up the "open GADT"TypeRep athat comes withTypeableand keep e.g. just thecastoperator, you get a higher-rank type:So existential types involving
Typeable(including yours) can't be meaningfully simplified.Existential types are also useful if you really can't control how many values of the quantified type you have, such as in
Simplifying this type according to the scheme above requires you to "find the
bs" inf band preapply theb -> ato them, butfis not fixed so that's not possible. (Whenfis aFunctoractuallyCoyoneda f a ~= f a, but there are still performance reasons for usingCoyoneda.) The only way out is the generic-but-unhelpful higher-ranked translation:With those examples done, I think this is a good place to put the real answer to your question: it is nontrivial to simplify away an existential type, or even to tell when that is possible. There are some "obvious" markers for types that can't be simplified, and even types that can be simplified may look very different afterwards. For one last example of that kind, the following type came up for me recently. I used it to represent navigation on a non-planar surface:
This type was easy to come up with: I thought of the cases and operations I needed to handle and wrote them down. However, the scheme for removing existentials actually applies pretty easily, and simplifies
MaptoIs this encoding more or less intuitive? In some cases, the choice of whether to use an existential is more about what you find easy to think about, not necessity.