gql: what is graphql?

I’m familiar with REST services. I’m not familiar with GraphQL.

My first thought (which might be wrong on multiple levels) is that GraphQL is a way of defining a services by having a single endpoint. Queries are declarations of the structure you want returned. Servers are functional mappings from types to values.

Racket doesn’t have a GraphQL implementation. In particular, I’m interested in a server implementation. (A client implementation looks like you squirt JSON documents at a server, and get JSON documents back. Although, the query language actually has a syntax, so it might not be strictly JSON…)

I’ll start with the GraphQL site’s explanation on execution. They use Star Wars examples, which is good, because I have seen Star Wars.

The authors of GraphQL say that you cannot execute a query without a type system, and then they present an example type system. I think this might also be a schema. The top level of the schema in GraphQL is the Query, which apparently all servers must expose. These are the entrypoints to any query that a client might make. So, in the example, it would seem you can query humans.

I’ve suggested an s-expression based syntax here.

(define-type Query
    (human     (id ID #:not-nullable) -> Human)

The syntax given says that a query for a human must include an id. So, given this schema, the following query would return no results (or, perhaps, an error?):

query {
  human {

would fail, but

query {
  human (id: 1002) {

would succeed.

The other types might look like

(define-type Human
    (name      -> String)
    (appearsIn -> (listof Episode))
    (starship  -> (listof Starship))

It may be that there are other ways to do this, but for the moment, I’m going to turn this into an “object.” In Javascript land, this means a “hash table.”

(define-syntax (define-type stx)
  (define-syntax-class field-defn
    #:description "field declaration"
    (pattern (field:id
              (~optional args #:defaults ([args #'()]))
              -> ret-type)))
  (syntax-parse stx
    [(_ type (fields f:field-defn ...))
     (with-syntax ([field-hashes
                    #`(let ()
                        (define f* (quote (f.field ...)))
                        (define a* (quote (f.args ...)))
                        (define rt* (quote (f.ret-type ...)))
                        (for/list ([_f f*]
                                   [_a a*]
                                   [_rt rt*])
                          (define fh (make-hash))
                          (hash-set! fh 'field _f)
                          (hash-set! fh 'args _a)
                          (hash-set! fh 'return-type _rt)
       #`(define type
           (let ()
             (define h (make-hash))
             (define _fh field-hashes)
             (hash-set! h 'fields _fh)

This is a syntax-parse macro. It allows me to define a new syntactic form in Racket, and generate code based on my new syntax. In this case, I’m going to generate a hash table from this:

(define-type Human
   (name      -> String)
   (appearsIn -> (listof Episode))
   (starship  -> (listof Starship))

that looks like this:

        (#hash((args . ((id ID!)))
               (field . name)
               (return-type . String))
         #hash((args . ())
               (field . appearsIn)
               (return-type . (listof Episode)))
         #hash((args . ())
               (field . starship)
               (return-type . (listof Starship))))))

where this gets me

From my reading, a GraphQL server is, in the first instance, an interpreter of queries. It carries out that interpretation against a backdrop of the type definitions, using the information in the schema to make sure the results coming back are correct. (And, perhaps, guiding the execution.)

A complete schema (or set of types) in this language I’m making up looks like this:

(define-type Starship
   (name -> String)))

(define-enum Episode

(define-type Human
   (name      -> String)
   (appearsIn -> (listof Episode))
   (starship  -> (listof Starship))

(define-type Query
   (human ([id ID!]) -> Human)

This renders out as a set of hash tables. The new form, define-enum, looks like this:

(define-syntax (define-enum stx)
  (syntax-parse stx
    [(_ type fields:id ...)
     #`(define type
         (let ([h (make-hash)])
           (define _f (quote (fields ...)))
           (for ([f _f]
                 [ndx (range (length _f))])
             (hash-set! h ndx f))

and produces a hash table like this:

'#hash((0 . NEWHOPE) (1 . EMPIRE) (2 . JEDI))

why macros?

I want as few macros as possible in this system. That seems to be a good rule when developing in Racket. It might be nice if I … leveraged the actual language a bit more. For example, at the moment, if I spell “Episode” as “Eipsode”, nothing is caught in Racket. It would be nice if things would fail immediately.

But, what this does (as a step… I’m just exploring here…) is get me to a space where I can write functions to do all of my work, using these definitions to power the functions. I can write a function that consumes a “type” and a hash table, and check if it is the right “type”. For example:

(define (->boolean o)
  (if o true false))

(define (is-type? typeh h)
     (hash-has-key? h 'type)
     (hash-has-key? typeh 'type)
     (hash-has-key? typeh 'fields)
     (equal? (hash-ref typeh 'type)
             (hash-ref h 'type))
     (let* ([fields-in-h (hash-keys h)]
             (cons 'type 
                   (map (λ (h) (hash-ref h 'field)) 
                        (hash-ref typeh 'fields)))])
       (andmap (λ (k)
                 (member k field-names-in-type))

When run on some test cases:

> (is-type? Starship (make-hash '((type . Starship)
                                  (name . "Aluminum Falcon"))))
> (is-type? Starship (make-hash '((type . Starship))))
> (is-type? Starship (make-hash))

The function isn’t complete, but it demonstrates a point. Given a “type” (which is a hash table) and another hash table, we can ask “is that hash table of the given type?” To be of a given type, it has to have a key called type, the value of that key must be the same as the value stored in the typeh hash, and all of the fields in the hash table in question must be in the typeh hash. (Because fields are nullable, I assume this means that a Starship does not have to have a name, but it can still be a starship. Some fields are not nullable, and really, this is where I need to do more reading on GraphQL and its schema language.)

So, put simply, a few macros lift me from syntax to data, and then I can write functions to process that data any way I want. And, I’d rather be writing functions than macros, because they’re… more obvious. The macros are a “little language,” and the functions are what will give meaning to that language.

things not handled…

There’s a ton of things not handled in this example. For example, I don’t actually handle types right. (The (listof x) is not handled at all, for example…) But, this was me doing a quick dive to see where it would lead me.

will this continue?

I don’t know. It might be nice to see if I can stand up a small GraphQL server. At least as a “proof of concept.” I still have my tbl exploration to continue, so I don’t want to get too distracted, but it seemed like something interesting to explore for a bit while I’m holed up at home.

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.