Skip to content

Commit

Permalink
Update docs related to hoistServer
Browse files Browse the repository at this point in the history
  • Loading branch information
phadej committed Oct 1, 2017
1 parent 4a6edd7 commit 15cc4f5
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 107 deletions.
1 change: 1 addition & 0 deletions cabal.project
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
packages:
servant/
servant-client/
servant-client-core/
servant-docs/
servant-foreign/
servant-server/
Expand Down
78 changes: 51 additions & 27 deletions doc/tutorial/Server.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ need to have some language extensions and imports:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
Expand Down Expand Up @@ -1057,75 +1058,71 @@ into something **servant** can understand?
If we have a function that gets us from an `m a` to an `n a`, for any `a`, what
do we have?
``` haskell ignore
newtype m :~> n = NT { ($$) :: forall a. m a -> n a}
``` haskell
type (~>) m n = forall a. m a -> n a
```
For example:
``` haskell
listToMaybeNT :: [] :~> Maybe
listToMaybeNT = NT listToMaybe -- from Data.Maybe
listToMaybe' :: [] ~> Maybe
listToMaybe' = listToMaybe -- from Data.Maybe
```
(`NT` comes from "natural transformation", in case you're wondering.)
Note that `servant` doesn't declare the `~>` type-alias, as the unfolded
variant isn't much longer to write, as we'll see shortly.
So if you want to write handlers using another monad/type than `Handler`, say the `Reader String` monad, the first thing you have to
prepare is a function:
``` haskell ignore
readerToHandler :: Reader String :~> Handler
readerToHandler :: Reader String a -> Handler a
```
Let's start with `readerToHandler'`. We obviously have to run the `Reader`
computation by supplying it with a `String`, like `"hi"`. We get an `a` out
from that and can then just `return` it into `Handler`. We can then just wrap
that function with the `NT` constructor to make it have the fancier type.
We obviously have to run the `Reader` computation by supplying it with a
`String`, like `"hi"`. We get an `a` out from that and can then just `return`
it into `Handler`.
``` haskell
readerToHandler' :: forall a. Reader String a -> Handler a
readerToHandler' r = return (runReader r "hi")
readerToHandler :: Reader String :~> Handler
readerToHandler = NT readerToHandler'
readerToHandler :: Reader String a -> Handler a
readerToHandler r = return (runReader r "hi")
```
We can write some simple webservice with the handlers running in `Reader String`.
``` haskell
type ReaderAPI = "a" :> Get '[JSON] Int
:<|> "b" :> Get '[JSON] String
:<|> "b" :> ReqBody '[JSON] Double :> Get '[JSON] Bool
readerAPI :: Proxy ReaderAPI
readerAPI = Proxy
readerServerT :: ServerT ReaderAPI (Reader String)
readerServerT = a :<|> b
where a :: Reader String Int
a = return 1797
readerServerT = a :<|> b where
a :: Reader String Int
a = return 1797
b :: Reader String String
b = ask
b :: Double -> Reader String Bool
b _ = asks (== "hi")
```
We unfortunately can't use `readerServerT` as an argument of `serve`, because
`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `Handler`. But there's a simple solution to this.
### Enter `enter`
### Welcome `hoistServer`
That's right. We have just written `readerToHandler`, which is exactly what we
would need to apply to all handlers to make the handlers have the
right type for `serve`. Being cumbersome to do by hand, we provide a function
`enter` which takes a natural transformation between two parametrized types `m`
`hoistServer` which takes a natural transformation between two parametrized types `m`
and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`.
In our case, we can wrap up our little webservice by using `enter
readerToHandler` on our handlers.
In our case, we can wrap up our little webservice by using
`hoistServer readerAPI readerToHandler` on our handlers.
``` haskell
readerServer :: Server ReaderAPI
readerServer = enter readerToHandler readerServerT
readerServer = hoistServer readerAPI readerToHandler readerServerT
app4 :: Application
app4 = serve readerAPI readerServer
Expand All @@ -1140,6 +1137,33 @@ $ curl http://localhost:8081/b
"hi"
```
### An arrow is a reader too.
In previous versions of `servant` we had an `enter` to do what `hoistServer`
does now. `enter` had a ambitious design goals, but was problematic in practice.
One problematic situation was when the source monad was `(->) r`, yet it's
handy in practice, because `(->) r` is isomorphic to `Reader r`.
We can rewrite the previous example without `Reader`:
```haskell
funServerT :: ServerT ReaderAPI ((->) String)
funServerT = a :<|> b where
a :: String -> Int
a _ = 1797
-- unfortunately, we cannot make `String` the first argument.
b :: Double -> String -> Bool
b _ s = s == "hi"
funToHandler :: (String -> a) -> Handler a
funToHandler f = return (f "hi")

app5 :: Application
app5 = serve readerAPI (hoistServer readerAPI funToHandler funServerT)
```
## Conclusion
You're now equipped to write webservices/web-applications using
Expand Down
70 changes: 29 additions & 41 deletions servant-server/src/Servant/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}

-- | This module lets you implement 'Server's for defined APIs. You'll
Expand All @@ -26,24 +27,10 @@ module Servant.Server
, layout
, layoutWithContext

-- * Enter
-- $enterDoc

-- ** Basic functions and datatypes
, enter
, (:~>)(..)
-- ** `Nat` utilities
, liftNat
, runReaderTNat
, evalStateTLNat
, evalStateTSNat
, logWriterTLNat
, logWriterTSNat
-- * Enter / hoisting server
, hoistServer

-- ** Functions based on <https://hackage.haskell.org/package/mmorph mmorph>
, hoistNat
, embedNat
, squashNat
, generalizeNat
, tweakResponse

-- * Context
Expand Down Expand Up @@ -106,12 +93,11 @@ module Servant.Server

) where

import Data.Proxy (Proxy)
import Data.Proxy (Proxy (..))
import Data.Tagged (Tagged (..))
import Data.Text (Text)
import Network.Wai (Application)
import Servant.Server.Internal
import Servant.Utils.Enter


-- * Implementing Servers
Expand Down Expand Up @@ -145,6 +131,30 @@ serveWithContext :: (HasServer api context)
serveWithContext p context server =
toApplication (runRouter (route p context (emptyDelayed (Route server))))

-- | Hoist server implementation.
--
-- Sometimes our cherished `Handler` monad isn't quite the type you'd like for
-- your handlers. Maybe you want to thread some configuration in a @Reader@
-- monad. Or have your types ensure that your handlers don't do any IO. Use
-- `hoistServer` (a successor of now deprecated @enter@).
--
-- With `hoistServer`, you can provide a function,
-- to convert any number of endpoints from one type constructor to
-- another. For example
--
-- /Note:/ 'Server' 'Raw' can also be entered. It will be retagged.
--
-- >>> import Control.Monad.Reader
-- >>> type ReaderAPI = "ep1" :> Get '[JSON] Int :<|> "ep2" :> Get '[JSON] String :<|> Raw :<|> EmptyAPI
-- >>> let readerApi = Proxy :: Proxy ReaderAPI
-- >>> let readerServer = return 1797 :<|> ask :<|> Tagged (error "raw server") :<|> emptyServer :: ServerT ReaderAPI (Reader String)
-- >>> let nt x = return (runReader x "hi")
-- >>> let mainServer = hoistServer readerApi nt readerServer :: Server ReaderAPI
--
hoistServer :: (HasServer api '[]) => Proxy api
-> (forall x. m x -> n x) -> ServerT api m -> ServerT api n
hoistServer p = hoistServerWithContext p (Proxy :: Proxy '[])

-- | The function 'layout' produces a textual description of the internal
-- router layout for debugging purposes. Note that the router layout is
-- determined just by the API, not by the handlers.
Expand Down Expand Up @@ -205,28 +215,6 @@ layoutWithContext :: (HasServer api context)
layoutWithContext p context =
routerLayout (route p context (emptyDelayed (FailFatal err501)))

-- Documentation

-- $enterDoc
-- Sometimes our cherished `ExceptT` monad isn't quite the type you'd like for
-- your handlers. Maybe you want to thread some configuration in a @Reader@
-- monad. Or have your types ensure that your handlers don't do any IO. Enter
-- `enter`.
--
-- With `enter`, you can provide a function, wrapped in the `(:~>)` / `NT`
-- newtype, to convert any number of endpoints from one type constructor to
-- another. For example
--
-- /Note:/ 'Server' 'Raw' can also be entered. It will be retagged.
--
-- >>> import Control.Monad.Reader
-- >>> import qualified Control.Category as C
-- >>> type ReaderAPI = "ep1" :> Get '[JSON] Int :<|> "ep2" :> Get '[JSON] String :<|> Raw :<|> EmptyAPI
-- >>> let readerServer = return 1797 :<|> ask :<|> Tagged (error "raw server") :<|> emptyServer :: ServerT ReaderAPI (Reader String)
-- >>> let nt = generalizeNat C.. (runReaderTNat "hi") :: Reader String :~> Handler
-- >>> let mainServer = enter nt readerServer :: Server ReaderAPI
--

-- $setup
-- >>> :set -XDataKinds
-- >>> :set -XTypeOperators
Expand Down
2 changes: 1 addition & 1 deletion servant-server/src/Servant/Server/Experimental/Auth.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ instance ( HasServer api context
type ServerT (AuthProtect tag :> api) m =
AuthServerData (AuthProtect tag) -> ServerT api m

hoistServer _ pc nt s = hoistServer (Proxy :: Proxy api) pc nt . s
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s

route Proxy context subserver =
route (Proxy :: Proxy api) context (subserver `addAuthCheck` withRequest authCheck)
Expand Down
Loading

0 comments on commit 15cc4f5

Please sign in to comment.