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

WIP: Issue 841: Rework of Verb #969

Closed
wants to merge 8 commits into from

Conversation

phadej
Copy link
Contributor

@phadej phadej commented Jun 2, 2018

this is WIP, but I open this for preview.

Note: #include "overlapping-compat.h" is removed from Servant.Server.Internal.

EDIT If I will work on something in ZuriHac, this will probably be it.

  • add status code to Redirect
  • add Redirect(s) to ComprehensiveAPI
  • Use Verb, no need for Verb'. We can break that.
  • Unify response and request headers
  • contentTypes in Verb or Result?
  • stream
  • AllCTMimeRender without OverlappingInstances

@phadej
Copy link
Contributor Author

phadej commented Jun 8, 2018

Copy link
Member

@jkarni jkarni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm loving this! Review just has some thoughts

routeR
:: Proxy api
-> Context context
-> Request
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be careful with Request, since the body may have already been consumed.
(Also, if I'm understanding this correctly, is there any use for the Request here?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should, but we can do "bad" things withRequest in HasServer too.

-- returns a Greet as JSON or redirects
type HelloEndpoint =
"hello" :> Capture "name" Text :> QueryParam "capital" Bool :>
Verb' 'GET (Redirect :> Result 200 '[JSON] Greet)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we maybe use :<|> instead of :>, since it's either one or the other, rather than one and the other?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be just me, but I find both of them equally confusing (not terribly though, I could get used to both being used there). To me, and in very vague terms (i.e don't pay too much attention to this), the :> let us chain constraints on the input (request), while :<|> lets us "or" constraints, but then neither corresponds to what feels like we're doing under that Verb, which would be something like "stating a few properties of the output". :& maybe? Not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used :> because we already have it.

:<|> because of the sum doesn't really make sense, as we can do products too. something like :>< or :* (as something "near enough to" ⊗), but those are also bad as the operator should convey it's fixity (not associative here!), and :> does it. :< would have "wrong" feeling.

Also having less operators is not a bad thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:<|> because of the sum doesn't really make sense, as we can do products too.

What I mean is that both :> and :<|> would be allowed inside the Verb. The first would correspond to products, the second to sums. That makes a little clearer what the expected handler type would be from the API type, and allows us in the instances to not have to pattern match on individual LHSs.

-- | TODO: TBW...
--
-- TODO: Add to ComprehensiveAPI
newtype Redirect = Redirect URI deriving (Typeable, Eq, Ord, Show)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should take a status code as well (since there are multiple types of redirect) or a type indicating one of the status codes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Will do.

-- | @Verb@ is a general type for representing HTTP verbs (a.k.a. methods). For
-- convenience, type synonyms for each verb with a 200 response code are
-- provided, but you are free to define your own:
--
-- >>> type Post204 contentTypes a = Verb 'POST 204 contentTypes a
data Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [*]) (a :: *)
type Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [*]) (a :: *)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very nice that Verb can be nearly backwards compatible in this way. But maybe it's Get, Put, etc that should have this sort of synonym, and we can just redefine Verb, since Verb is much less user-facing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are probably right. I don't remember writing Verb that often myself.


instance HasServerR api => HasServerR (Redirect :> api) where
type ServerR (Redirect :> api) = Either Redirect (ServerR api)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strange that both this and the above instance are needed. And also a little sad that we might end up with nested Eithers. If we switch to having both :> and :<|> inside the Verb, I guess we could just map uniformly over :<|> into a sumtype? And something to consider is maybe using an extensible sum type?

Copy link
Contributor Author

@phadej phadej Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have unconditional and optional Redirects. There fore two instances.

One option is to have yet another class for so

instance (HasClientR api, HasClientRR combinator)
  => HasClient (combinator :> api)

That would be cool as we won't need as meny FlexibleInstances (we can do same factoring for HasClient (combinator :> api) instances as well. We'll end up with 4 classes then. It's only a naming problem.

Ending up with nested sums (Redirect) and products (Headers) is inevitable, and I'll keep api simple. It's not even that contrived to have

"endpoint" :> Verb' 'Get
  (  Headers '[ Header "X-API-Version"]
  :> Redirect
  :> Headers '[ Header "X-API-Something"]
  :> Result 200 '[JSON] Value)

@phadej
Copy link
Contributor Author

phadej commented Jun 12, 2018

API design question, when Verb' is renamed, should we have some aliases to Verb 'GET, Verb 'POST etc. as Get ctypes a is alias to Verb 'GET (Result 200 ctypes a).

So

  • GET enum value
  • Get ctypes a = Verb 'GET (Result 200 ctypes a) = ??? (Result 200 ctypes a)
  • ??? = Verb 'GET, I suspect this to be used often enough to have own type alias

@phadej
Copy link
Contributor Author

phadej commented Jun 12, 2018

Another question is how to deal with "right extensions". As I commented above I think we don't need to complicate things with HList and HSum (NP / NS), however:

  • Headers uses custom type: Headers hs a
  • Redirect uses "anonymous" Either

I think we should be consistent and use one of following options consistently:

Separate custom product/sum for each combinator

-- combinator
data Headers hs

-- term
data ResponseHeaders hs a = ResponseHeaders (HList hs) a -- with field names though

Note: that Headers and ResponseHeaders have different kinds. We have similar happening for
Header and RequestArgument on the request side.

Side comment This change let us unify handling of request and response combinators, and I will do that. Users can decide whether they want single Header or plural Headers. Former is very simple when you have one header, latter is more convenient when you have many headers to parse/respond.

Note: we'll need two types for right and left:

data Headers hs

data ResponseHeaders hs a = MkResponseHeaders (HList hs) a
newtype RequestHeaders hs = MkRequestHeaders (HList hs)

type ServerT (Headers hs :> api) m = RequestHeaders hs -> ServerT api m
type ServerR (Headers hs :> api)   = ResponseHeaders hs (ServerR api)

the Pro of this approach is that specific types often result in better error messages.

Embrace (a, b) and Either a b

This is uniform to how we deal on the left, there we use ->

data Headers hs = MkHeaders (HList hs)

type ServerT (Headers hs :> api) m = Headers hs -> ServerT api m
type ServerR (Headers hs :> api)   = (Headers hs, ServerR api)

-- ^ note: we can still have convinient pair constructor with specific type for that:
mkWithHeaders :: HList hs ->  a -> (Headers hs, a)

This in fact let us use same name for combinator and data type.


@phadej phadej added this to the 0.15 milestone Jun 12, 2018
@alpmestan
Copy link
Contributor

API design question, when Verb' is renamed, should we have some aliases to Verb 'GET, Verb 'POST etc. as Get ctypes a is alias to Verb 'GET (Result 200 ctypes a).

On the one hand, it feels like Get should just be the general one, and Get200 or something like that would be what we currently call Get, i.e status code fixed to 200, no headers. On the other hand, that'd "uselessly" break quite a lot of code. I'm honestly not sure what the best option is. As witnessed by the hard time I have coming up with a good name for the general Get.

Another question is how to deal with "right extensions".

I really like the simplicity of your second suggestion and how it makes ServerR nicely mirror ServerT.

@jkarni
Copy link
Member

jkarni commented Jun 13, 2018

I agree NP/NS is a little complicated. But it would be sad to have to have handlers use return multiple times (instead of just once) to e.g. return a value from a handler that has multiple optional Redirects. And the handlers won't compose well anymore.

The way I think of it, with sums at least, it'd be nice to have an mtl/extensible-effects-like interface. So:

handler1 :: (ThrowsErr1 m) => Handler m A
handler2 :: (ThrowsErr2 m) => A -> Handler m B

handler3 :: (ThrowsErr1 m, ThrowsErr2 m) => Handler m B
handler3 = handler1 >>= handler2


handler4 :: <throws who knows what m> => Handler m Int
handler4 = return 5

handler5 :: (Redirect, ...) => Handler m A
handler5 = throwError $ inject $ Redirect ...

Would be really nice. I know there are problems with this:

  • It doesn't really extend in an obvious way to products
  • We need types to be concrete at some point. If we make it concrete in the associated type synonym (as we've done so far), then the above only works if the end-user gives a more general type than the inferred one to her handlers.

@alpmestan
Copy link
Contributor

alpmestan commented Jun 13, 2018

I agree that having to chain multiple returns would be quite annoying. But your bullet points there make me thing this is not the sweet spot we're looking for here. This is quite a tricky design problem :-)

@arianvp
Copy link
Member

arianvp commented Jun 13, 2018

I think NS/NP is the way to go here honestly. However, perhaps we want a default mode where people don't have to bother with heterogeneous lists. that is, you should be able to use servant as you used to, but only need to bother about heterogeneous lists when they want to have the return types encoded in the API.

@phadej
Copy link
Contributor Author

phadej commented Jun 13, 2018

Multiple redirects (I guess with different statuses?) can be handler with new combinator Redirects (plural) which bundles them into single NS, while :> (or whatever) is encoded as NS.

We can have A * (B + (C * D)) like expressions, potentially.

About :> and :<|>. We could have, rewriting my previous example

"endpoint" :> Verb' 'Get
  (  Headers '[ Header "X-API-Version"]
  :> (Redirect :<|>
       Headers '[ Header "X-API-Something"]
    :> Result 200 '[JSON] Value))

but it opens a possibility to

"endpoint" :> Verb' 'Get (Result 200 '[JSON] Value :<|> Result 200 '[CSV] [Text])

and it doesn't work out. Client tells it wants JSON, so we cannot return CSV. We'd need to have a pair handler there, not an Either (it's not our choice, but theirs!)! It might be possible to encode, but it will definitely make implementation more complex, for a little benefit.

I mentioned , as it overloads × and +. But actually i'm seeking for a (right-)module like thing of a "tensor production", but I guess there aren't a notation for those.

So in summary:

  • :<|> is problematic
  • :> or :>< or :<> or .. choice is irrelevant, but we already have :>, but we can add something else too. Note: than we'd need to add TypeError (Text "use :><") => HasServerR (comb :> api), as I'm sure many (me including) will write wrong operator in wrong place).

@phadej
Copy link
Contributor Author

phadej commented Jun 13, 2018

About plurality, examples:

Verb' 'Get (Redirect 300 :> Redirect 301 :> Result ...)
Verb' 'Get (Redirects '[300,301] :> Result ...)
Verb' 'Get (Header "foo" Int :> Header "bar" String :> Result ...)
Verb' 'Get (Headers '[Header "foo" Int, Header "bar" String] :> Result ...)

And IMO it depends on the details which, nested sums or products, or bundled NS/NP are "better". We can have all of those above.


Different issue of smashing different combinators into same NS or NP will be also difficult to implement. Returning Either Redirect (Either (NS Error '[err1, err2] MyValue)) in a handler is not /that/ bad. I think better than either

NS I '[Redirect, Error err1, Error err2, I MyValue]
Either (NS I '[Redirect, Error err1, Error err2]) MyValue

I think that term level inconvenience is way easier to abstract than to make some crazy stuff to produce "flatter" handler types.


Note I prototyped current design in https://github.com/phadej/servant-tiny/tree/more-comb, feel free to explore desing space based on it (it's simpler), but I won't bike-shedding prevent me proceeding with current design. I.e. MTL / Magical NS-smashing etc is out without "existential proof" it will work. (Note how little extensions are used there!)

@phadej
Copy link
Contributor Author

phadej commented Jun 13, 2018

How's about

  • GET enum value
  • Get ctypes a = Verb 'GET (Result 200 ctypes a) = Get' (Result 200 ctypes a)
  • Get' = Verb 'GET

using prime is a little boring, but we already do that for QueryParam'...

@jkarni
Copy link
Member

jkarni commented Jun 13, 2018 via email

@phadej
Copy link
Contributor Author

phadej commented Jun 13, 2018

Then we'll need to have a way to bypass Content-Type negotiation for NoContent or unconditional Redirect. Abusing empty contentTypes to mean any might work, though feels dirty. Generic programming might become difficult (i.e. one have to be careful with Filter like constructs, but I guess one needs to be careful today already).

@jkarni
Copy link
Member

jkarni commented Jun 13, 2018

Is it horrible if we fail with Not Acceptable in a handler that only redirects/returns no content if the list of content-types does not include the requested content-type? That is, NoContent and Redirect would both be instances of all serializations, but you'd still have to mention the ones you want to support.

@phadej
Copy link
Contributor Author

phadej commented Jun 13, 2018 via email

@phadej
Copy link
Contributor Author

phadej commented Jun 23, 2018

I refactored servant-server part a bit with some of @jkarni suggestions. Now you can write (as in greet example):

Verb 'GET '[JSON] (Result 400 Text :<|> Result 200 Greet)

IMHO it's enough for core servant atm, checked-exceptions could have own Result-like combinator.

@phadej
Copy link
Contributor Author

phadej commented Jun 23, 2018

FWIW

it's possible to have

type GET = Verb 'GET

but I'm not sure if that will be more confusing than helpful?

@phadej
Copy link
Contributor Author

phadej commented Jun 24, 2018

servant-client and

Verb 'GET '[JSON] (Result 400 Text :<|> Result 200 Greet)

I think we'll need to dispatch on status code, so we'll need some kind of Alternative for ClientM.

@alpmestan
Copy link
Contributor

Yes, we need to do some kind of "inversed/dual routing". Fortunately it's very simple, as the status code is the only thing we will ever look at to decide what to try to decode the response to.

@phadej phadej mentioned this pull request Jun 28, 2018
12 tasks
@arianvp
Copy link
Member

arianvp commented Jul 12, 2018

I think we do not need something like NS at all! Given we know what status codes to be able to return,
we can construct the injection ourselves!
A simple Lkup type family is sufficient here and some nice Singleton magic

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}

import Data.Proxy
import GHC.TypeLits


data StatusCode = X200 | X401 | X403

data SStatusCode :: StatusCode -> * where
  S200 :: SStatusCode 'X200
  S401 :: SStatusCode 'X401
  S403 :: SStatusCode 'X403

data Get (xs :: [(StatusCode, *)])

type family Lkup (x :: StatusCode) (xs :: [(StatusCode, *)]) :: * where
  Lkup x '[] = TypeError ('Text "Status code not specified in API type!")
  Lkup 'X200 ('( 'X200, x) ': xs) = x
  Lkup 'X401 ('( 'X401, x) ': xs) = x
  Lkup 'X403 ('( 'X403, x) ': xs) = x
  Lkup a (x ': xs) = Lkup a xs

data OneOf :: [(StatusCode, *)] -> * where
  For :: SStatusCode x -> Lkup x xs -> OneOf xs
  

class HasServer layout where
  type Server layout :: *
  route :: Proxy layout -> Server layout -> [String] -> Maybe (IO String)


instance HasServer (Get xs) where
  type Server (Get xs) = IO (OneOf xs)


data User = User String String
data AuthError =  PasswordWrong

type API =  Get '[ '( 'X200, User), '( 'X401, AuthError)]


checkPassword :: IO Bool
checkPassword = undefined
hasPermissions :: IO String
hasPermissions = undefined

handleLogin :: Server API
handleLogin = do
  correctPassword <- checkPassword
  if  correctPassword
    then pure $ For S200 (User "arian" "lol123")
    else pure $ For S401 PasswordWrong

If the user writes a server that is not conformant to the API, then you get a nice error:

  
handleLoginNotConformToAPI :: Server API
handleLoginNotConformToAPI = do
  correctPassword <- checkPassword
  if  correctPassword
    then pure $ For S200 (User "arian" "lol123")
    else pure $ For S403 PasswordWrong

servant.hs:63:26: error:
     Status code not specified in API type!
     In the second argument of For, namely PasswordWrong
      In the second argument of ($), namely For S403 PasswordWrong
      In the expression: pure $ For S403 PasswordWrong
   |
63 |     else pure $ For S403 PasswordWrong
   |                          ^^^^^^^^^^^^^

We get a nice API for clients as well, However, Haskell complains about
incomplete patterns if we omit matching on S403. It's not smart enough to figure it out because it's not Agda :(

-- Lets for now assume that Server and Client type family are the same
-- which is a fair assumption
client :: IO ()
client = do
  -- x :: (OneOf '['('X200, User), '('X401, AuthError)])
  x <- handleLogin
  case x of
    For S200 (User name _pw) -> putStrLn name
    For S401 PasswordWrong -> putStrLn "wrong password" 
    --  x :: Lkup x '['('X200, User), '('X401, AuthError)]
    --  which is uninhabited, which the exhausitveness checker should figure out
    --  but it doesn't
    For S403 x -> error "Haskell complains about incomplete patterns if we omit"

However, if we try to use the result of S403 we get a nice type error again! So that's cool

misbehavingClient :: IO ()
misbehavingClient = do
  -- x :: (OneOf '['('X200, User), '('X401, AuthError)])
  x <- handleLogin
  case x of
    For S200 (User name _pw) -> putStrLn name
    For S401 PasswordWrong -> putStrLn "wrong password" 
    For S403 x -> putStrLn x


servant.hs:77:28: error:
     Status code not specified in API type!
     In the first argument of putStrLn, namely x
      In the expression: putStrLn x
      In a case alternative: For S403 x -> putStrLn x
   |
77 |     For S403 x -> putStrLn x
   |   

@fisx
Copy link
Member

fisx commented Jun 15, 2020

I think since we have https://github.com/haskell-servant/servant-uverb now, this can be closed? Please reopen if I'm wrong.

@fisx fisx closed this Jun 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants