I'd suggest that handle this the same way as GHC handles numeric conversions (e.g. fromIntegral
), which is via rewrite rules.
If you look in GHC.Real, you find
-- | general coercion from integral types
fromIntegral :: (Integral a, Num b) => a -> b
fromIntegral = fromInteger . toInteger
{-# RULES
"fromIntegral/Int->Int" fromIntegral = id :: Int -> Int
#-}
The default is a lot of round-tripping for integral types, but fortunately that never happens because there are RULEs for all library-provided integral types.
There are many more RULEs specified in GHC.Int for example to handle the rest of the conversions. You'll find a similar setup for other similar functions (e.g. realToFrac
).
Now there's one major problem with your use-case, which is that it's often difficult for RULEs to match on class methods. There are two ways around this. The first is to define a common type (e.g. Integer
in GHC's code), and provide class methods to convert to and from that type. Then write a general-purpose conversion function (e.g. fromIntegral
), use that everywhere, and have your RULEs match on it.
The other approach is to do something like this:
instance PixelComponent CUChar where
blackWhite x = (minBound x, maxBound x)
toInt = id
toRealFrac = fromIntegral
{-# INLINE fromComponent #-}
fromComponent = toCUChar
toCUChar :: PixelComponent a => a -> CUChar
toCUChar = ...
{-# RULES "fromComponent/CUChar->CUChar" toCUChar = id :: CUChar -> CUChar #-}
The former is what GHC does, so it's likely to work well. I've been using the latter approach recently and haven't had any issues though, so either one should work.