Über Technik und Philosophie

GraphQL Haskell got a new maintainer

3. September 2019

Introduction

Some years ago there was an unfinished GraphQL implementation. The project was on hold for the last few years. Recently I took over the maintenance to continue the development. The git repository has moved: https://github.com/caraus-ecms/graphql. It is still unfinised and work-in-progress (very well usable in basic cases, though), but now under active development.

The main difference to the typical “type-safe” GraphQL alternatives is that this implementation aims to be more low-level and more flexible.

What has changed

These are some of the recent changes (below I give a few examples for some of these):

  • Mutations support
  • Almost every public symbol has at least basic documentation
  • Lists and nullable types can be arbitrary nested
  • Pretty-printer for GraphQL query AST
  • Megaparsec (instead of Attoparsec) is used for parsing
  • Error handling
  • Continious Integration and fixed tests
  • Documents with multiple operations are supported
  • Block string support

Error handling

Given:

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.Trans.Except (throwE)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text)
import Language.GraphQL
import Language.GraphQL.Schema
import Language.GraphQL.Trans

throwing :: Resolver IO
throwing = scalar "hello" (ActionT $ throwE "Error message" :: ActionT IO Text)

schema :: NonEmpty (Resolver IO)
schema = throwing :| []

graphql schema "{ hello }" produces:

{
  "data": {
    "hello": null
  },
  "errors":[{
    "message": "Error message"
  }]
}

Pretty-printer for GraphQL query AST

{-# LANGUAGE OverloadedStrings #-}

import qualified Language.GraphQL.Encoder as Encoder
import qualified Language.GraphQL.Parser as Parser
import Text.Megaparsec (parse)

Encoder.document Encoder.pretty <$> parse Parser.document "" "{ hello }"

prints:

{
  hello
}

You can replace Encoder.pretty with Encoder.minified to print a minified version of the query.

Megaparsec (instead of Attoparsec) is used for parsing

Error messages provide more context information now and look like:

{
  "errors": [{
    "line": 1,
    "message": "unexpected '\"'\\nexpecting ')' or '_'",
    "column": 17
   }]
}

Strong typing versus strong typing

GraphQL provides a type system. But I think there is some misconception when we‘re talking about the strong typing in GraphQL.

For this section we define a simple GraphQL type:

type Query {
  id: Int
  name: String
}

And the according Haskell representation:

data Query = Int String

GraphQL schema provides a contract between the client and the server. If I have a query

{
 id
 name
}

and the query is valid according to the schema, then I have a guarantee on the client side that I get a response with two fields, “id” and “name”, and that these are an integer and a string, respectively. This is “request-time” typing.

But this guarantee doesn‘t apply to my static, server-side language, just because the types returned by my resolvers depend on the query, they aren‘t known at compile-time, but first at runtime.

Strictly speaking I can‘t have a resolver with a type like query :: Query, because the client could request just the name:

{
 name
}

and it would be a different type, a subtype of Query.

GraphQL schema is there to validate the query (at runtime), and not to provide a static type system for the server implementation. Types returned by the server are dynamic.

This situation is similar to other, better known, query languages like SQL. Yes, SQL provides types and you can map a SQL table to a Haskell type, but the database schema tells you nothing about the types you get in your program, since you can request only a subset of a table. Joins, subqueries make the situation even more complicated.

Of course there can be solutions for GraphQL.

  • The most obvious solution is to initialize the complete type Query and let the library to pick the fields the client requested. It isn‘t nice if your Query type is more complex and consists of other types, that may require additional queries to the database, the client may not be intersted in.
  • You can wrap each Query type in a Maybe (e.g. data Query = (Maybe Int) (Maybe String)), so the fields not requested by the client can be set to Nothing. It looks a bit ugly to me.
  • Or you can define each type as a set of resolvers (e.g. data Query = (IO Int) (IO String), IO is there to allow arbitrary actions, like queries to a database, in reality it would be something more complex) and call the resolver only if the appropriate field is requested. This one looks most promising to me, especially taking Haskell’s lazyness in account. But I don’t have a concrete plan currently, how it can be nicely implemented and combined with the GraphQL Schema Definition Language.

What I’m not saying is that using a GraphQL schema for resolver typing is a bad idea; I just don’t beleive, it’s going to work in every edge case. At the end of the day you have to make a compromise: either you choose type safity and lose flexibility or you prefer flexibility and lose type safity. The same choice you have to met when you choose an SQL library (raw queries versus some kind of ORM).

Looking to the future

Code breakage

I’ve heard some rumors that haskellers are known to break the code too easily. I should be honest here: I’m one of that people. Or let us put it this way: I prefer breaking changes to maintaining an ugly design. I already know some places where I have to break the code at some point. For example resolvers should get additional information about the queries, execution functions should get the schema to be able to validate the query… And sometimes there is no easy migration path.

Next releases will often contain breaking changes. I always try to deprecate functions or data structures where possible before removing or renaming them. And of course I bump major version to make this breakages visible to the user.

Conclusion

I don’t have big plans or promises. I’m regulary working on the library and implementing missing features step by step. Since I’m using GraphQL from Haskell in a private project, the development is mostly directed by the requirements of that project and it will remain like this at least for the rest of this year.

It doesn’t mean that I disappear for months or don’t response to issues or pull requests. If you find the library useful and need some features that aren’t implemented currently, you’re welcome to help.