Search code examples
haskelltype-level-computationservant

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


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?


Solution

  • 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