Evidence for equality constraints on typeclass methods isn’t erased
Generally, I expect a ~ b
constraints to have no runtime cost, so I was surprised to discover that they sometimes do. If I understand correctly, constraints of type a ~ b
are boxed versions of a ~# b
, the latter of which have no runtime representation. Ordinarily, this boxing and unboxing is eliminated via worker/wrapper just like any other boxing, but worker/wrapper can only be performed at call sites to known functions, so equality constraints on typeclass methods are not erased unless the method is specialized. This program illustrates that:
{-# LANGUAGE ConstrainedClassMethods, TypeFamilies #-}
module M where
type family F a
class C a where
m :: F a ~ Int => a -> Bool
f :: (C a, F a ~ Int) => a -> Bool
f x = not (m x)
Compiling with -O -ddump-simpl
reveals that the call to m
inside F a ~ Int
really does pass a boxed equality.
This seems silly to me. As far as I can tell, equalities are aggressively forced, so it would be entirely safe to perform a worker/wrapper-esque transformation to the C
constructor itself. Specifically, GHC could desugar C
into the following:
newtype C a = $WC { $wm :: F a ~# Int -> a -> Bool }
C :: (F a ~ Int => a -> Bool) -> C a
C m = $WC { $wm = m `seq` \co -> m (Eq# co) }
m :: (C a, F a ~ Int) => a -> Bool
m ($WC { $wm }) (Eq# co) = $wm co
The equality argument would be erased on both ends via the same mechanism that exists today for worker/wrapper DataCons, and the resulting program would be slightly more efficient.
Maybe there is some problem with this scheme I’m not seeing; it’s entirely possible. I don’t know all the subtleties. But seeing as it seems totally sound to me based on the investigation I’ve done so far, and since equalities not being erased was so surprising to me, I feel like it isn’t entirely unreasonable to consider this a bug rather than a missing feature (though that is admittedly arguable).