haskell-servant / servant

Servat is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.82k stars 412 forks source link

(:<|>) is not Associative #1643

Open ChrisPenner opened 1 year ago

ChrisPenner commented 1 year ago

Hello wonderful servant maintainers,

I guess whether this is actually a bug depends on your expected behaviour, but I wasn't easily able to find the expected behaviour written down anywhere (maybe I just missed it); Given that :<|> is defined in Servant.API.Alternative I expected it to be associative, so I was quite surprised when I learned it isn't!

Here's a small reproduction which demonstrates this:

type FlatAPI =
  ("a" :> "b" :> Post '[JSON] Int)
    :<|> ("a" :> "b" :> Delete '[JSON] String)
    :<|> ("c" :> "d" :> Post '[JSON] Int)
    :<|> ("c" :> "d" :> Delete '[JSON] String)

type NestedAPI =
  ("a" :> "b" :> (Post '[JSON] Int :<|> Delete '[JSON] String))
    :<|> ("c" :> "d" :> (Post '[JSON] Int :<|> Delete '[JSON] String))

handlePost :: Handler Int
handlePost = pure 1

handleDelete :: Handler String
handleDelete = pure "hello"

server :: Server FlatAPI
server =
  handlePost
    :<|> handleDelete
    :<|> handlePost
    :<|> handleDelete

nestedServer :: Server NestedAPI
nestedServer =
  (handlePost :<|> handleDelete) -- < these brackets are required.
    :<|> handlePost
    :<|> handleDelete

I had assumed that FlatAPI and NestedAPI were equivalent, but it turns out that NestedAPI requires you to manually group your server implementations to match the bracketing on your API. Remove the brackets causes it to fail to compile.

If this is expected, that's fine, just letting you know that I got tripped up here 😄

alpmestan commented 1 year ago

This is sadly a consequence of "just" representing endpoints as an agglomerate of :<|> separated stuffs, instead of having this "reduce" to some internal representation that will "quotient out" the kind of difference you are talking about (e.g generating a [Any] or Vector Any or something underneath).

I wrote a tiny package for "flattening" things in case that's useful: https://hackage.haskell.org/package/servant-flatten-0.2/docs/Servant-API-Flatten.html