Decryption of type promotion DataKind in the Servant library

I am trying to complete a tutorial for servant , web-level DSL level. The library uses the DataKind extension DataKind .

At the beginning of this tutorial, we will find the following line that defines the endpoint of the web service:

 type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] 

I don't understand what it means to have a string and arrays in a type signature. I also don't understand what the mark ( ' ) before '[JSON] means.

So my questions come down to what is the type / type of string and arrays, and how is this later interpreted when it rotates to the WAI endpoint?


As a note, the consistent use of Nat and Vect in describing DataKinds leaves us with an unsatisfactory set of examples to look at when trying to understand this material. I think I read this example at least a dozen times in different places, and I still don’t feel that I understand what is happening.

+5
source share
2 answers

When you turn on DataKinds you get new types that are automatically created based on regular data type definitions:

  • If you have data A = BT | CU data A = BT | CU , now you get a new view of A and new types of 'B :: T -> A and 'C :: U -> A , where T and U are new types of similarly shot T and U types
  • If there is no ambiguity, you can write B for 'B , etc.
  • Level-level strings all have the same Symbol type, so you have, for example, "foo" :: Symbol and "bar" :: Symbol as valid types.

In your example, "users" and "sortby" are both types in the form of Symbol , JSON is a (old-fashioned) type in the form * (defined here ), and '[JSON] is a type in the form [*] , that is, it is singleton type list (it is equivalent to JSON ': '[] in the same way [x] equivalent to x:[] in general).

The type [User] is a regular type in the form * ; it is just a User s list type. This is not a one-line list of types.

+3
source

Let Build Servant

Goals

Our goals will be the Servant's Goals:

  • Indicate our REST API as one type of API
  • Implement the service as one side effect (read: monadic) Function
  • Use real types to model resources, only serializing them to a smaller type at the very end, for example. JSON or Bytestring
  • Follow the general WAI (Web Application Interface) interface, which is the most used in Haskell HTTP infrastructures.

Threshold crossing

Our initial service would be simply / , which returns a list of User in JSON.

 -- Since we do not support HTTP verbs yet we will go with a Be data User = ... data Be a type API = Be [User] 

Although we still have to write one line of value level code, we have already presented our REST service enough - we just cheated and did it at the type level. This seems interesting to us and, for the first time in a long time, we hope for website programming again.

We need a way to convert this to WAI type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived . There is not enough space to describe how WAI works. Basics: we are a given request object and a way to build response objects, and we are expected to return a response object. There are many ways to do this, but a simple choice is this.

 imp :: IO [User] imp = return [ User { hopes = ["ketchup", "eggs"], fears = ["xenophobia", "reactionaries"] } , User { hopes = ["oldies", "punk"], fears = ["half-tries", "equivocation"] } ] serve :: ToJSON a => Be a -> IO a -> Application serve _ contentIO = \request respond -> do content <- contentIO respond (responseLBS status200 [] (encode content)) main :: IO () main = run 2016 (serve undefined imp) 

And it really works. We can run this and twist it and get the expected answer back.

 % curl 'http://localhost:2016/' [{"fears":["xenophobia","reactionaries"],"hopes":["ketchup","eggs"]},{"fears":["half-tries","equivocation"],"hopes":["oldies","punk"]}]% 

Note that we never built a value of type Be a . We used undefined . The function itself completely ignores the parameter. There is really no way to construct a value of type Be a , since we never defined data constructors.

Why is there even a Be a parameter? The poor simple truth is that we need that variable a . He tells us what our type of content will be, and he allows us to establish that Aeson's sweet limitation.

Code: 0Main.hs .

: <| > s on the road

Now we are challenging ourselves to develop a routing system where we can have separate resources in separate places in a fake URL hierarchy folder. Our goal will be to support this type of service:

 type API = "users" :> Be [User] :<|> "temperature" :> Int 

To do this, you first need to enable TypeOperators and DataKinds extensions. As pointed out by @Cactus answer, data types allow you to store data at the type level, and GHC is built in with String type literals. (This is great, since defining strings in a level type is not my idea of ​​fun.)

(We will also need PolyKinds , so GHC can get this type. Yes, we are now deep in the heart of the jungle extensions.)

Then we need to call smart definitions for :> (a subdirectory operator) and :<|> (a disjunction operator).

 data path :> rest data left :<|> right = left :<|> right infixr 9 :> infixr 8 :<|> 

Did I say smart? I meant the dead simply. Note that we gave :<|> a type constructor. This is because we will glue our monadic functions together to realize the disjunction and ... oh, it's just easier to give an example.

 imp :: IO [User] :<|> IO Int imp = users :<|> temperature where users = return [ User ["ketchup", "eggs"] ["xenophobia", "reactionaries"] , User ["oldies", "punk"] ["half-tries", "equivocation"] ] temperature = return 72 

Now let's pay attention to the special serve task. no longer can we write a serve function that relies on the API being Be a . Now that we have a bit of DSL at the type level for RESTful services, it would be nice if we could somehow match the types and implement another serve for Be a , path :> rest , and left :<|> right . And there is!

 class ToApplication api where type Content api serve :: api -> Content api -> Application instance ToJSON a => ToApplication (Be a) where type Content (Be a) = IO a serve _ contentM = \request respond -> do content <- contentM respond . responseLBS status200 [] . encode $ content 

Pay attention to the use of related data types here (which, in turn, requires us to include either TypeFamilies or GADTs ). Although the Be a endpoint has an implementation of type IO a , this will not be enough to carry out a disjunction. As low-paying and lazy functional programmers, we simply throw out another layer of abstraction and determine the level type of a function called Content , which takes an API type and returns a Content api type.

 instance Exception RoutingFailure where data RoutingFailure = RoutingFailure deriving (Show) instance (KnownSymbol path, ToApplication rest) => ToApplication (path :> rest) where type Content (path :> rest) = Content rest serve _ contentM = \request respond -> do case pathInfo request of (first:pathInfoTail) | view unpacked first == symbolVal (Proxy :: Proxy path) -> do let subrequest = request { pathInfo = pathInfoTail } serve (undefined :: rest) contentM subrequest respond _ -> throwM RoutingFailure 

Here we can break the lines of code:

  • We guarantee an instance of ToApplication for path :> rest if the compiler can guarantee that path is a level symbol (which means it [can be displayed before String with symbolVal among other things) and that ToApplication rest exists.

  • When the request arrives, we will match the correspondence by pathInfos with determine success. In case of failure, we will do a lazy thing and throw an uncontrolled exception in IO .

  • If successful, we recurs at the type level (cue laser noise and fog) using serve (undefined :: rest) . Note that rest is a "smaller" type than path :> rest , just like when you map a template to a data constructor, you end up with a "smaller" value.

  • Before recursing, we destroy the HTTP request with a convenient write update.

Note that:

  • type Content maps the path :> rest functions to Content rest . Another level-level recursion! Also note that this implies that the extra path on the route does not change the type of resource. This is consistent with our intuition.

  • The IO exception throw is not a great library design β„’, but I will leave it to you to fix this problem. (Hint: ExceptT / throwError .)

  • Hopefully we are slowly motivating the use of DataKinds here with string characters. The ability to represent strings in a type level allowed us to use types to match pattern matching in a type.

  • I use lenses for packing and unpacking. It's just faster for me to crack these SO answers with lenses, but of course you can just use the pack from the Data.Text library.

Good. One more example. Take a breath Take a break.

 instance (ToApplication left, ToApplication right) => ToApplication (left :<|> right) where type Content (left :<|> right) = Content left :<|> Content right serve _ (leftM :<|> rightM) = \request respond -> do let handler (_ :: RoutingFailure) = serve (undefined :: right) rightM request respond catch (serve (undefined :: left) leftM request respond) handler 

In this case, we

  • Guarantee ToApplication (left :<|> right) , if the compiler can guarantee blah blah blah, you will get it.

  • Enter another entry in the type Content function. Here is a line of code that allows us to build up to the type IO [User] :<|> IO Int and successfully compile the compiler in the process of resolving the instance.

  • Catch the exception we threw above! When the exception occurs on the left, we go to the right. Again, this is not Great Library Design β„’.

Run 1Main.hs and you should be able to curl like this.

 % curl 'http://localhost:2016/users' [{"fears":["xenophobia","reactionaries"],"hopes":["ketchup","eggs"]},{"fears":["half-tries","equivocation"],"hopes":["oldies","punk"]}]% % curl 'http://localhost:2016/temperature' 72% 

Give and take

Now let's demonstrate the use of type-level lists, another feature of DataKinds . We will add our data Be to save a list of types that the endpoint can return.

 data Be (gives :: [*]) a data English data Haskell data JSON -- | The type of our RESTful service type API = "users" :> Be [JSON, Haskell] [User] :<|> "temperature" :> Be [JSON, English] Int 

Let's also define a class that matches a list of types. An endpoint can provide a list of MIME types that an HTTP request can accept. We will use Maybe to indicate failure here. Again, not Great Library Design β„’.

 class ToBody (gives :: [*]) a where toBody :: Proxy gives -> [ByteString] -> a -> Maybe ByteString class Give give a where give :: Proxy give -> [ByteString] -> a -> Maybe ByteString 

Why two separate classes? Well, we need one for the view [*] , which is the type of the list of types, and one for the view * , which is the view of only one type. Just as you cannot define a function that takes something for an argument that is both a list and a non-list (since it will not check the type), we cannot determine the type that takes something for the argument, what is both a level list and a non-type list (as it will not check). If we kindclasses ...

Take a look at this class in action:

 instance (ToBody gives a) => ToApplication (Be gives a) where type Content (Be gives a) = IO a serve _ contentM = \request respond -> do content <- contentM let accepts = [value | ("accept", value) <- requestHeaders request] case toBody (Proxy :: Proxy gives) accepts content of Just bytes -> respond (responseLBS status200 [] (view lazy bytes)) Nothing -> respond (responseLBS status406 [] "bad accept header") 

Very nice. We use toBody as a way of abstracting the calculations of converting a value of type a to the base bytes that the WAI wants. In the event of a failure, we are simply mistaken with 406, one of the most esoteric (and therefore more interesting to use) status codes.

But wait, why use type lists at all? Because how did we do this before we are going to map the templates to our two constructors: nil and cons.

 instance ToBody '[] a where toBody Proxy _ _ = Nothing instance (Give first a, ToBody rest a) => ToBody (first ': rest) a where toBody Proxy accepted value = give (Proxy :: Proxy first) accepted value <|> toBody (Proxy :: Proxy rest) accepted value 

Hope this makes sense. A malfunction occurs when the list is started empty before finding a match; <|> guarantees a short circuit to success; toBody (Proxy :: Proxy rest) is a recursive case.

We will need some fun instances of Give to play.

 instance ToJSON a => Give JSON a where give Proxy accepted value = if elem "application/json" accepted then Just (view strict (encode value)) else Nothing instance (a ~ Int) => Give English a where give Proxy accepted value = if elem "text/english" accepted then Just (toEnglish value) else Nothing where toEnglish 0 = "zero" toEnglish 1 = "one" toEnglish 2 = "two" toEnglish 72 = "seventy two" toEnglish _ = "lots" instance Show a => Give Haskell a where give Proxy accepted value = if elem "text/haskell" accepted then Just (view (packed . re utf8) (show value)) else Nothing 

Start the server again and you should curl do it like this:

 % curl -i 'http://localhost:2016/users' -H 'Accept: application/json' HTTP/1.1 200 OK Transfer-Encoding: chunked Date: Wed, 04 May 2016 06:56:10 GMT Server: Warp/3.2.2 [{"fears":["xenophobia","reactionaries"],"hopes":["ketchup","eggs"]},{"fears":["half-tries","equivocation"],"hopes":["oldies","punk"]}]% % curl -i 'http://localhost:2016/users' -H 'Accept: text/plain' HTTP/1.1 406 Not Acceptable Transfer-Encoding: chunked Date: Wed, 04 May 2016 06:56:11 GMT Server: Warp/3.2.2 bad accept header% % curl -i 'http://localhost:2016/users' -H 'Accept: text/haskell' HTTP/1.1 200 OK Transfer-Encoding: chunked Date: Wed, 04 May 2016 06:56:14 GMT Server: Warp/3.2.2 [User {hopes = ["ketchup","eggs"], fears = ["xenophobia","reactionaries"]},User {hopes = ["oldies","punk"], fears = ["half-tries","equivocation"]}]% % curl -i 'http://localhost:2016/temperature' -H 'Accept: application/json' HTTP/1.1 200 OK Transfer-Encoding: chunked Date: Wed, 04 May 2016 06:56:26 GMT Server: Warp/3.2.2 72% % curl -i 'http://localhost:2016/temperature' -H 'Accept: text/plain' HTTP/1.1 406 Not Acceptable Transfer-Encoding: chunked Date: Wed, 04 May 2016 06:56:29 GMT Server: Warp/3.2.2 bad accept header% % curl -i 'http://localhost:2016/temperature' -H 'Accept: text/english' HTTP/1.1 200 OK Transfer-Encoding: chunked Date: Wed, 04 May 2016 06:56:31 GMT Server: Warp/3.2.2 seventy two% 

Hooray!

Note that we stopped using undefined :: t and switched to Proxy :: Proxy t . Both are hacks. Haskell calling functions allow us to specify values ​​for value parameters, but not types for type parameters. Sad asymmetry. Both undefined and Proxy are encoding methods; enter parameters at the value level. Proxy is able to do this without runtime, and t in Proxy t is poly-like. ( undefined is of type * , so undefined :: rest won't even check.)

Remaining work

How can we get to the full competitor Servants?

  • We need to break Be to Get, Post, Put, Delete . Note that some of these verbs also now accept data in the form of a body request. Modeling content types and query bodies at the type level requires similar type machines.

  • What if the user wants to model their functions as something other than IO , for example, a stack of monad transformers?

  • A more accurate but more complex routing algorithm.

  • Hey, now that we have a type for our API, would it be possible to create a service client? Something that makes HTTP requests to a service that obeys the API description, rather than creating the HTTP service itself?

  • Documentation

    . Making sure everyone understands that all these hijinks are type .;)

This mark

I also don’t know what the mark (') before "[JSON] means.

The answer is unclear and stuck in the GHC manual in section 7.9 .

Since constructors and types have the same namespace, with progress you can get ambiguous type names. In these cases, if you want to refer to an advanced constructor, you must prefix its name with a quote.

With -XDataKinds, Haskell types and types are initially promoted to views and use the same convenient syntax at the type level, albeit with a quote prefix. For level lists of two or more elements, such as the signature foo2 above, the quote may be omitted because the meaning is unambiguous. But lists of a single or null element (as in foo0 and foo1) require a quote because the types [] and [Int] have existing values ​​in Haskell.

This is how complicated all the code that we had to write above was, and much more, in addition, due to the fact that programming at the level level is still a second-class citizen in Haskell, in contrast to language-dependent languages ​​( Agda, Idris, Coq). The syntax is strange, there are many extensions, the documentation is sparse, the errors are nonsense, but programming at the boy level is boy-boy fun.

+12
source

All Articles