Functional GraphQL 1 - Specs and typelevel parsing

MS
Mike Solomon

CEO

4th Sep 2020

One of the common points of friction in GraphQL-land is making sure resolvers are conformant to specs. There are three main strategies I've seen to do this:

  1. Runtime validation of IO like in Apollo and Absinthe.
  2. Code generation, like Prisma and GraphQL code generator.
  3. Deriving the schema from the resolvers or db, which is available in purescript-graphql and underpins services like 8base.

They all have their disadvantages:

  1. Runtime validation requires lots of testing and manual upkeep.
  2. Code generation requires constantly merging generated code and dealing with stubbed methods.
  3. Deriving the schema from the resolvers is my favorite so far, but IMO it puts the control in the wrong direction. A schema should be influenced equally by consumers, and putting too much control in the hands of producers makes development less fluid.

I'd like to talk about a different strategy: generation of resolver types.

The basic idea is that you have a schema, and the compiler uses the schema to generate the type of your resolver. That way, when the schema updates, the code no longer compiles, and getting the code to compile makes it schema-conformant.

This strategy is possible in any strongly-typed language. For example, in TypeScript, GraphQL code generator will generate a resolver type that looks like this:

This is already a vast improvement, but it still has a few issues:

  1. Important parts of business logic cannot be known by a type generator. For example, if you want custom directives to influence authentication or need a more nuanced context, a type generator becomes clunky.
  2. You need an extra layer of translation from input types to application-level types. For example, the String me above may need to be a full-fledged User object in the application.
  3. You lose the benefits of code generation in places where application logic is boring and repeatable.

So, while the generation of resolver types is helpful, we can do better. For the rest of this article, I'd like to talk about compile-time generation of resolver types.

What is compile-time generation of types?#

Compile-time generation of types means using the compiler to generate one type from another type. While that many sound a bit strange, there's nothing about it that's conceptually different than generating one value from another value. In programming, for example, we can do:

At the type-level (switching to PureScript) this becomes:

In the first example, we're guaranteed that any value that goes into toString will come out as a string on the other end. In the second example, we're guaranteed that any type that goes into ToString will come out as String on the other end.

Here, we've the same toString function in JavaScript for free, and in addition, we've gotten a compile-time validator forceString. To see why, let's look at what happens when you use forceString with anything other than a string.

Compiler 1

If we hover over it, you'll see that the compiler has "solved" what forceString must take.

Compiler 2

Using ToString, we've pulled a type out of thin air. If you look at forceString, nowhere in the definition does it say b must be a String. The compiler figures it out from the implementation of ToString.

In the same way, the compiler will figure out what type our GraphQL resolver needs to be from our GraphQL spec.

A slightly-more realistic example#

In the following example, we'll use purescript-typelevel-parser to turn symbols into types. In PureScript, Symbol is a kind. A kind is just a family of types. The types that inhabit kind Symbol are every unicode string. Thankfully, the binary of the PureScript compiler doesn't contain every unicode string (that'd be a pretty big compiler!). The compiler creates a type for each unicode String on-the-fly. So the type "a" is of kind Symbol just like the value "a" is of type String.

Instead of GraphQL, in this example, we'll use a spec called TypeQL. This (fabricated) spec is a series of lowercase strings separated by &. The general idea is that it defines keys that point to a type. Keys of what? Who knows! Perhaps a dictionary, perhaps a database - that's for us to decide when we implement the spec. What type do the keys point to? Who knows! Perhaps Int, perhaps String.

One example would be gold&silver&bronze pointing to Int, another would be earth&wind&fire pointing to Boolean. Like a GraphQL spec, our TypeQL spec doesn't mean anything. It is just a container of information. Our program will give it meaning, just like a GraphQL resolver gives a spec meaning.

An engineer has been tasked with taking a TypeQL spec and creating a PureScript implementation that can work for any type. So, for example, if we get a spec earth&wind&fire, then we want to be able to automatically generate a type { earth :: a, wind :: a, fire :: a }, where a can be any type (Int, String, etc).

Let's start with some imports.

Now, we define our spec. In this case, it will be python&java&javascript. In the next article, this'll be a full blown GraphQL spec. Notice how "python&java&javascript" is a Symbol, not a String, because of the identifier type.

The next step is defining a parser. In most cases, you never have to actually write a parser as it is done for you (ie Apollo parses GraphQL). The same will be true of our GraphQL parser in the next article, but I want to show you how it's done so that you can follow a full example

Using the primitives of purescript-typelevel-parser, we define a list of lowercase values separated by the separator & that accurately describes the TypeQL spec.

Once we have a parser result, we need to define how it translates into a type that our application understands (continuing in GraphQL-speak, we need to define the type of our resolver). In this case, we will take the parser result and turn it into a row where every key points to something of type i. For example, foo&bar&baz with type Int will turn into a type { foo :: Int, bar :: Int, baz :: Int }.

Now, we can create our typelevel validator. The validator just passes through an object, but in doing so, validates at compile-time that the object conforms to our spec. For example, { python: 1, javascript: 2, java: 3 } conforms to our spec.

Let's imagine that we now add a language (say purescript). Our spec becomes python&java&javascript&purescript. What happens to languages? Let's see!

As we hoped, the compiler gets angry.

Compiler

But, because it deep down the PureScript compiler is nice, it gives us a helpful error message letting us know what we did wrong.

Compiler love

Now we can fix it!

Conclusion and next steps#

In this article, I've shown how typelevel-programming allows you to take a spec (ie TypeQL) and use it to generate a type. We use the type to create a validator that makes sure our code is always conformant with the spec. It's also allowed us to use custom business-logic (in this case, using Int as the indexed type of our record).

In the next article in this series, which will come out in mid-September 2020, I'll show how this strategy can be used to build type-safe GraphQL resolvers. In the meantime, if you're building a GraphQL API, you should sign up for Meeshkan! We do automated testing of GraphQL APIs.

Create a free account

Our novel approach combines functional programming and machine learning to execute thousands of tests against your GraphQL API and find mission-critical bugs and inconsistencies. Regisration is free, and don't hesitate to reach out - we'd love to learn more about what you're building!

Newer postOlder post

Don’t miss the next post!

Absolutely no spam. Unsubscribe anytime.

Company

About usContactPricingT&C