Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better type errors for NamedRoutes without Generic instance #1641

Conversation

amesgen
Copy link
Contributor

@amesgen amesgen commented Jan 21, 2023

If you forget to derive Generic for your API types in the context of named routes, you get rather unhelpful error messages. For example, consider

data FooAPI mode = FooAPI
  { foo :: mode :- "foo" :> Post '[JSON] String
  }
  -- deriving stock (Generic)

fooClient :: Client ClientM (NamedRoutes FooAPI)
fooClient = client (Proxy @(NamedRoutes FooAPI))

which yields

Test.hs:20:13: error:
    • Overlapping instances for HasClient
                                  ClientM
                                  (Servant.API.Generic.GToServant (GHC.Generics.Rep (FooAPI AsApi)))
        arising from a use of ‘client’
      Matching instances:
        instance [overlappable] (Servant.Client.Core.RunClient.RunClient m,
                                 (TypeError ...)) =>
                                HasClient m api
          -- Defined in ‘Servant.Client.Core.HasClient’
        instance (HasClient m a, HasClient m b) => HasClient m (a :<|> b)
          -- Defined in ‘Servant.Client.Core.HasClient’
        instance Servant.Client.Core.RunClient.RunClient m =>
                 HasClient m EmptyAPI
          -- Defined in ‘Servant.Client.Core.HasClient’
        ...plus 13 others
        ...plus 19 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
      (The choice depends on the result of evaluating ‘GHC.Generics.Rep,
                                                       Servant.API.Generic.GToServant’
       To pick the first instance above, use IncoherentInstances
       when compiling the other instance declarations)
    • In the expression: client (Proxy @(NamedRoutes FooAPI))
      In an equation for ‘fooClient’:
          fooClient = client (Proxy @(NamedRoutes FooAPI))
   |
20 | fooClient = client (Proxy @(NamedRoutes FooAPI))
   |             ^^^^^^

Test.hs:20:13: error:
    • Couldn't match type: Client
                             n
                             (Servant.API.Generic.GToServant (GHC.Generics.Rep (FooAPI AsApi)))
                     with: Servant.API.Generic.GToServant
                             (GHC.Generics.Rep (FooAPI (AsClientT n)))
        arising from a use of ‘client’
    • In the expression: client (Proxy @(NamedRoutes FooAPI))
      In an equation for ‘fooClient’:
          fooClient = client (Proxy @(NamedRoutes FooAPI))
   |
20 | fooClient = client (Proxy @(NamedRoutes FooAPI))
   |             ^^^^^^

for me on GHC 9.2.4. One might spot the Rep type family here from GHC.Generics, but it is certainly not obvious at all, the first message in particular. This can be particularly bad when you have many nested APIS, residing in completely different modules.

In this PR, we use a trick from this blog post: https://blog.csongor.co.uk/report-stuck-families/

With it, the first error message gets much simpler:

Test.hs:20:13: error:
    • Named routes require a Generic instance for FooAPI
    • In the expression: client (Proxy @(NamedRoutes FooAPI))
      In an equation for ‘fooClient’:
          fooClient = client (Proxy @(NamedRoutes FooAPI))
   |
20 | fooClient = client (Proxy @(NamedRoutes FooAPI))
   |             ^^^^^^

Test.hs:20:13: error:
    • Couldn't match type: Client
                             n
                             (Servant.API.Generic.GToServant (GHC.Generics.Rep (FooAPI AsApi)))
                     with: Servant.API.Generic.GToServant
                             (GHC.Generics.Rep (FooAPI (AsClientT n)))
        arising from a use of ‘client’
    • In the expression: client (Proxy @(NamedRoutes FooAPI))
      In an equation for ‘fooClient’:
          fooClient = client (Proxy @(NamedRoutes FooAPI))
   |
20 | fooClient = client (Proxy @(NamedRoutes FooAPI))
   |             ^^^^^^

The error messages for other snippets such as

fooApp :: Application
fooApp = serve (Proxy @(NamedRoutes FooAPI)) FooAPI {foo = pure "foo"}

also improve in the same manner.

I added the error-reporting constraint as a superclass constraint to the various instances of NamedRoutes. There might be further places to apply this pattern, or even a way to also make the second error message better (naively adding ErrorIfNoGeneric to e.g. GenericServant does not work, as then certain call sites of toServant/fromServant then no longer compile).


As this approach relies non-trivially on type family evaluation semantics, I can't outrule that this might yield type errors in contexts where one tries to use NamedRoutes with a not-yet-concrete api, but playing around, I couldn't find anything, and a relatively large internal code base using servant and various add-on packages also compiled fine.

@tchoutri
Copy link
Contributor

@amesgen It's a fantastic addition, thank you very much! Tell me if you need help with the doctest CI :)

@amesgen
Copy link
Contributor Author

amesgen commented Jan 21, 2023

Tell me if you need help with the doctest CI :)

Yeah, I think I could use some help there; I can reproduce the CI failure locally, but I am a bit mystified as I don't modify Servant.API.TypeLevel and it does not use named routes 🤔 I will try to tinker with it a bit more, but feel free to e.g. directly push to this branch if you see what is going wrong.

@amesgen
Copy link
Contributor Author

amesgen commented Jan 21, 2023

Maybe it is due to something like sol/doctest#327? Personally, I've been using cabal-docspec ever since I became aware of it as it was much more robust for me.

@tchoutri
Copy link
Contributor

Can't blame you, cabal-docspec really is the best one out there.

@amesgen amesgen force-pushed the custom-error-for-named-routes-no-generic branch from ca1dfa5 to 420d633 Compare January 22, 2023 23:40
@amesgen
Copy link
Contributor Author

amesgen commented Jan 22, 2023

Moved the custom error message to the TypeErrors module; which seems to somehow have fixed the doctest failures.

@gdeest
Copy link
Contributor

gdeest commented Mar 9, 2023

@amesgen Thanks for your contribution ! I find it very helpful.

As this approach relies non-trivially on type family evaluation semantics, I can't outrule that this might yield type errors in contexts where one tries to use NamedRoutes with a not-yet-concrete api, but playing around, I couldn't find anything, and a relatively large internal code base using servant and various add-on packages also compiled fine.

So I am guessing this will only be a problem if one tries to call serveWithContext on NamedRoutes routes where routes itself isn't concrete. I don't think that's a common use-case, and adding constraints on Rep (routes ()) into context might be enough to unstuck the type family application and solve the issue. So I think it is safe to merge this PR.

@gdeest gdeest merged commit 38f519a into haskell-servant:master Mar 9, 2023
@amesgen amesgen deleted the custom-error-for-named-routes-no-generic branch March 9, 2023 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants