Inspecting records whose fields' types are the result of type-level computations

110 Views Asked by At

This came up in the context of the servant library, but the issue reappears in other contexts.

Servant allows you to define named routes using a record, like this:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE TypeOperators #-}
import GHC.Generics
import Servant

type API = NamedRoutes Counter

data Counter mode = Counter
  { counterPost :: mode :- Capture "stuff" Int :> PostNoContent,
    counterGet :: mode :- Get '[JSON] Int
  }
  deriving stock (Generic)

The type Server API will perform some type-level computation, which evaluates to the type

ghci> :kind! Server API
Server API :: *
= Counter (AsServerT Handler)

I would like a way to "peek into" the record type and inspect the final types of each field, which here would be the result of evaluating AsServerT Handler :- Capture "stuff" Int :> PostNoContent and AsServerT Handler :- Get '[JSON] Int.

But specifying those two expressions separatedly is inconvenient. I would like to pass the type Server API to... something, and get the evaluated type of all fields in return. Does such functionality exist?

1

There are 1 best solutions below

0
danidiaz On BEST ANSWER

It seems that one way of getting the fields' types is through the generic representation:

ghci> :kind! Rep (Server API)
Rep (Server API) :: * -> *
= M1
    D
    ('MetaData "Counter" "Main" "main" 'False)
    (M1
       C
       ('MetaCons "Counter" 'PrefixI 'True)
       (M1
          S
          ('MetaSel
             ('Just "counterPost")
             'NoSourceUnpackedness
             'NoSourceStrictness
             'DecidedLazy)
          (K1 R (Int -> Handler NoContent))
        :*: M1
              S
              ('MetaSel
                 ('Just "counterGet")
                 'NoSourceUnpackedness
                 'NoSourceStrictness
                 'DecidedLazy)
              (K1 R (Handler Int))))

Kind of verbose, but it works and plays well with the "Eval" code lens in VSCode:

enter image description here

For less verbosity, a Generics-based helper could produce a more manageable output. Using my by-other-names package, we can define:

recordFields ::
  forall r.
  (Generic r, GHasFieldNames (Rep r), GRecord Typeable (Rep r)) =>
  [(String, TypeRep)]
recordFields =
  Data.Foldable.toList $
    gRecordEnum @Typeable @(Rep r) gGetFieldNames typeRep

Which, put to use:

enter image description here