Home > Back-end >  Can I get a list of constructors (like with `Typeable`, `Data`), but based on type inference instead
Can I get a list of constructors (like with `Typeable`, `Data`), but based on type inference instead

Time:10-20

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.

  • Related