Search code examples
haskelltypesdependent-typeservant

Combining types for a servant endpoint


Using servant, I've got a type like the following, but more complex:

type MyAPI endpointTail result = "blah" :> Capture "a" A :> endpointTail :> Get '[JSON] result

Which means I can do things like this:

MyAPI "hello" HelloT

but when I do:

MyAPI "hello/world" HelloWorldT

Servant silently fails to produce my endpoint correctly, presumably because it doesn't expect a literal slash

When I try:

MyAPI ("hello" :> "world") HelloWorldT

I get a type error because :> is only defined when the right argument is of kind *, which "world" is not, it's of kind Symbol.

It seems for servant to work correctly, the :> has to be applied in a right associative fashion, one can't just add brackets willy nilly. So what I think I need is something like this:

type MyAPIF endpointTailF result = "blah" :> Capture "a" A :> endpointTail (Get '[JSON] result)

Note endpointTail is now a type function, endpointTailF.

Then I could do

type Blah t = "hello" :> "world" :> t

MyAPIF Blah HelloWorldT

But now I've got the issue of the compiler saying it doesn't like Blah being partially applied.

So in summary, I've got a nice reusable type which I'd like to keep using, I'd just like to be able to pass more parameters. If these were values I'd be able to do this easily, by passing a function "continuation" style, but I'm not sure of the solution in the type world. Any ideas?


Solution

  • Your deductions are all correct. Unless you want to crawl down the rabbit hole that is singleton defunctionalization (the usual method for working with partially applied type functions), you'll want to avoid treating type functions as arguments. Use a plain data structure instead.

    For example, if you write a type function that prefixes a type using a list of URL path segments:

    type PrefixWith :: [Symbol] -> Type -> Type
    type family PrefixWith segs t where
      PrefixWith (seg:segs) t = seg :> PrefixWith segs t
      PrefixWith '[] t = t
    

    and then incorporate a call to this function within your API:

    type MyAPI endpointTail result
      = "blah"
      :> Capture "a" A
      :> PrefixWith endpointTail (Get '[JSON] result)
    

    then this lets you write:

    MyAPI ["hello","world"] HelloWorldT
    

    Full code example:

    {-# LANGUAGE DataKinds #-}
    {-# LANGUAGE DeriveGeneric #-}
    {-# LANGUAGE OverloadedStrings #-}
    {-# LANGUAGE StandaloneKindSignatures #-}
    {-# LANGUAGE TypeApplications #-}
    {-# LANGUAGE TypeOperators #-}
    {-# LANGUAGE TypeFamilies #-}
    
    import Servant
    import Data.Aeson
    import Data.Kind
    import GHC.Generics
    import GHC.TypeLits
    import Network.Wai.Handler.Warp
    
    type PrefixWith :: [Symbol] -> Type -> Type
    type family PrefixWith segs t where
      PrefixWith (seg:segs) t = seg :> PrefixWith segs t
      PrefixWith '[] t = t
    
    type MyAPI endpointTail result
      = "blah"
      :> Capture "a" A
      :> PrefixWith endpointTail (Get '[JSON] result)
    
    data HelloWorldT = HelloWorld { success :: Int } deriving (Generic)
    instance ToJSON HelloWorldT
    type A = Int
    
    main :: IO ()
    main = run 8080 (serve (Proxy @(MyAPI ["hello", "world"] HelloWorldT))
                           (\n -> return (HelloWorld n)))
    -- example URL, http://localhost:8080/blah/1/hello/world