Mu-Haskell 0.3: GraphQL and a simplified API

Mu-Haskell 0.3: GraphQL and a simplified API

We have been quite busy during the last two months adding new features to Mu-Haskell, and we are happy to announce the 0.3 release of the library! With this version, you are no longer restricted to gRPC as your transport layer; you can expose your servers using GraphQL. We also focused on some problems with how you describe services, which led to a revamp of that part of the API.

GraphQL is the new kid on the block 😎

RPC-like mechanisms for communication, like gRPC, are quite simple in describing which data you want. Essentially, your only choice is which method you want to call, and this prescribes which data you must include as payload, and the format of the data you will get. But this might waste resources. Consider this as an illustration: you might only be interested in the titles of books from an online retailer, but you’re forced to see the names of the authors as well. And, more importantly, you might be interested in a whole tree of resources, sub-resources, sub-sub-resources, etc., but you don’t want to make dozens of requests to get them all.

GraphQL provides a much richer interface to describe what you want. Take the following query from the docs:

{
  author(name: ".*Ende.*") {
    name
    books {
      title
    }
  }
}

We want to obtain the author that satisfies the given regular expression ".*Ende.*". That’s where you would stop with RPC. But in GraphQL, you also provide the schema of the data: here we want to just the name of the author and the title of their books to be returned.

GraphQL services are described by a schema, as it is the case with gRPC or Avro services. Here is a library schema that supported the operation from the query above. Using this schema, you can create complex queries, like, “all the books of the author of The Lord of the Rings.” The reason why the code is littered with exclamation marks is because they mean “not optional,” and this is the default for Haskell types.

type Query {
  author(name: String! = ".*"): Author
  book(name: String! = ".*"): [Book!]!
}
type Book {
  title: String!
  author: Author!
}
type Author {
  name: String!
  books: [Book!]!
}

As for the rest of the protocols, you can directly import this schema in the world of Mu with a single line.

graphql "LibrarySchema" "LibraryService" "library.graphql"

Implementing a GraphQL server is slightly more involved than a gRPC one, though. Instead of one function per method, you have to implement one function per field in the schema. Here is the server that implements the previous schema (already using the new Server API we are going to talk about below).

server = resolver
  ( object @"Book"   ( field  @"title"   bookTitle
                     , field  @"author"  bookAuthor )
  , object @"Author" ( field  @"name"    authorName
                     , field  @"books"   authorBooks )
  , object @"Query"  ( method @"book"    findBookByTitle
                     , method @"authors" findAuthorByName )
  )
  where
    findBookByTitle  :: Text -> m [BookId]
    findAuthorByName :: Text -> m [AuthorId]
    bookTitle        :: BookId -> m Text
    bookAuthor       :: BookId -> m AuthorId
    ...

Depending on the query, the minimum number of fields are requested. In other words, each of those functions are called only if the corresponding data is requested. Alas, splitting the fields among different functions makes it harder to create optimizations such as “obtain exactly the desired fields from the database in one go.” We are exploring solutions to this problem by either exposing richer data about the request, or providing automatic batching in Haxl style.

If you want to know more about GraphQL in Mu, check the introduction or the more thorough documentation.

gRPC-to-GraphQL magic 🧙

Mu has one single language for describing service, whether it be gRPC or GraphQL. That means that you can take one service defined using a Protocol Buffers schema, implement it, and then expose it as both gRPC and GraphQL servers. The following block exposes server using gRPC in port 8080, and using GraphQL in port 50053.

main :: IO ()
main = concurrently
  (runGRpcApp msgProtoBuf 8080 server)
  (runGraphQLAppQuery 50053 server (Proxy @"Query"))

You can check many more examples in the Mu-Haskell repo.

Server definition by name 📛

Defining the server for a service in Mu used to look like a list of functions separated by pipes and ending in H0. Each of those functions is the handler of one of the methods in the service.

server = Server (method1 :<|>: method2 :<|>: ... :<|>: H0)

This design follows Servant’s, so many Haskellers are familiar with it. Alas, it has three main drawbacks:

  1. It’s quite magical in how it works. Forgeting to add H0 at the end results in a terrible error message, which may puzzle even advanced programmers.
  2. It’s very hard to map each function to the corresponding method. This means that, whenever you need to add or remove an item in the list, you have to very carefully find its position.
  3. We ask programmers to import their service definitions instead of writing them by hand in Haskell. That means that one needs to know in which order those definitions appear in the resulting type-level information (as written? In alphabetic order? What about imports in Avro files?).

The new API tackles problems (2) and (3) by explicitly annotating each handler with the method it corresponds to. For problem (1), we provide an interface based on tuples, so you do not need to remember weird symbolic operators.

server = singleService ( method @"method1" method1
                       , method @"method2" method2 )

Of course, there’s a balance between conciseness (now you need to replicate the names of the methods) and ease of use (now you can use any order you want for your handlers). You can still define servers in the old style if you prefer.

Quite technical: no more higher-kinded data 🗂️

This part describes a change to the internals of Mu-Haskell, which should not come to the surface during normal usage of the library. Yet, we find this discussion to be interesting.

Version 0.2 used higher-kinded data in the internal representation of values within the library. This corresponds to the w parameter in the Term data type and the conversion type classes. The whole idea was that, by giving different values to that parameter, we could distinguish behaviors such as “optional by default” (from Protocol Buffers) or “present by default” (from Avro), and also define GraphQL resolvers in that fashion.

After some discussion about Protocol Buffers optional policy and a failed attempt at specifying resolver in that way, it became clear that higher-kinded data had been a red herring for us. As a result, version 0.3 strips out that type argument completely.

For the particular scenario of Protocol Buffers, we have decided to implement their optionality policy at the moment of importing the schema to the type-level. Whereas before you had Maybe T regardless of the corresponding schema type T, now this Maybe is missing for primitive types such as strings and integers, since Protocol Buffers specifies that a default value should be used there.

Conclusion

With the release of Mu-Haskell 0.3, we are really true to our goal of providing a unified interface to different communication protocols. Not only at the serialization layer – we already supported JSON, Avro, and Protocol Buffers – but also in the protocol layer. We also try to provide the best possible API. We’d love to hear any further suggestions about how to continue in that direction.

The active development of Mu-Haskell is proudly sponsored by 47 Degrees, a Functional Programming consultancy with a focus on the Scala, Kotlin, Swift, and Haskell programming languages.

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.