I had a hard time putting together the question. Let's try step by step:
I have a Haskell type class (that represents a keyboard layout fyi):
class Data key => Palantype key where
-- the instance has to provide this mapping
keyCode :: key -> Char
-- but the reverse is required, too:
toKeys :: Char -> [key]
I can provide a (not-so-efficient) default implementation for toKeys
based on Typeable
and Data
:
-- | override for efficiency
toKeys :: key -> Char -> [key]
toKeys k c =
let t = dataTypeOf k
ks = fromConstr . indexConstr t <$> [1..(maxConstrIndex t)]
m = foldl (\m k -> Map.insertWith ( ) (keyCode k) [k] m) Map.empty ks
in fromMaybe [] $ Map.lookup c m
... and the above code works. Nice.
However, there is a problem. The default implementation requires key
as first argument, the reason being: Data
requires a run-time representation of the type. This is provided by DataType
, using dataTypeOf :: a -> DataType
.
I have to adjust the type signature of toKeys
and always provide a not-so-meaningful dummy key. I understand that this is how Data.Data
works. But is there a way to get the same magic based on the type variable key
?
There is the function typeRep
in Data.Typeable
that seems to work that way:
typeRep :: forall proxy a. Typeable a => proxy a -> TypeRep
But all the workings of Data
(fromConstr
, indexConstr
, maxConstrIndex
) rely on the run-time representation DataType
(for a reason?).
An elegant solution using Data.Proxied
:
toKeys :: Char -> [key]
toKeys c =
let t = dataTypeOfProxied (Proxy :: Proxy key)
ks = fromConstr . indexConstr t <$> [1..(maxConstrIndex t)]
m = foldl (\m k -> Map.insertWith ( ) (keyCode k) [k] m) Map.empty ks
in fromMaybe [] $ Map.lookup c m
CodePudding user response:
A quick test in GHCi:
> dataTypeOf (undefined :: Int)
DataType {tycon = "Prelude.Int", datarep = IntRep}
This reveals that dataTypeOf
does not really need a runtime value, and the first argument is only used for its type. You can (and should) write something like
toKeys :: forall key . Data key => Char -> [key]
toKeys c =
let t = dataTypeOf (undefined :: key)
...
In my opinion, this interface is not how it should be today, but we still have it because of historical reasons. When Data
was designed, I guess, we had no AllowAmbiguousTypes, TypeApplications
so we used "unevaluated" arguments and/or proxies.
If Data
were designed today, I guess we would have the ambiguous type
dataTypeOf :: forall a . Data a => DataType
and we would use that as dataTypeOf @key
.