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?
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:
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: