Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 913
danidiaz Avatar asked Oct 27 '25 22:10

danidiaz


1 Answers

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

like image 119
danidiaz Avatar answered Oct 30 '25 12:10

danidiaz