Skip to content

Instantly share code, notes, and snippets.

@gyzerok gyzerok/elm-graphql.md
Last active Nov 28, 2016

Embed
What would you like to do?
Thoughts on implementing GraphQL in Elm

GraphQL in Elm

Before going down to some problem statement and requirement I'd like to tell you what makes Elm for me a better choice upon JavaScript and why I moving towards adopting it for productions usage.

I can make some changes in my codebase, run elm-make and see all the places where I should do subsequent changes. Statically. Ave compiler and static types!

The goal of this paper is to describe why proposition of using different types for different queries would result in almost the same pitfalls I have in JavaScript with GraphQL.

Requirements

I've been using GraphQL in development since it was released. These are my requirements to Elm's version of it which may result in fantastic developer experience.

Ability to use GraphQL schema types as a first-class citizens

Example (this code is not valid, assume it like pseudocode):

type alias User =
  { id : String
  , name : String
  , age : Int
  }
  
query1 = """
  query {
    viewer {
      id,
      name
    }
  }
"""
  
view1 : User -> Html
view1 user =
  -- display user
  
query2 = """
  query {
    viewer {
      id,
      age
    }
  }
"""
  
view2 : User -> Html
view2 user =
  -- display user

Now imagine that user becomes robot and he's name is now an integer. What I'm going to do is go and change User type.

type alias User =
  { id : String
  , name : Int
  , age : Int
  }

After running compiler I would see all the places where I need to change my code to support updated User type. The key advantage about it is while my application and schema grows my ability to refactor code remains the same.

Now imagine situation where you define specific type for specific query. In this case compiler can't help you. The bigger your application become the more difficult would be changing your schema. This is how things work currently in JavaScript for this.

Possible solution

We can define User type as partials in the schema.

module Schema.User where

type alias ID = String
type alias Name = String
type alias Nickname = Maybe String
type alias Age = String

Now we can represent various fields easily

import Schema.User

type alias User =
  { id : Schema.User.ID
  , name : Schema.User.Name
  }
  
query : Query -> User

and create different User types for different queries.

import Schema.User

type alias User =
  { id : Schema.User.ID
  , nickname : Schema.User.Nickname
  , age : Schema.User.Age
  }
  
query : Query -> User

But since we use types declared in the Schema.User we can solve our first requirement. Now if we change field type in the schema we gonna get superios compiler help!

The unknown

How to represent links as soon as we do not declare User type in the first place? How to represent nested fields with similar types?

type alias Friends = List user

And now we can create nesting as simple as

type alias User =
  { id : Schema.User.ID
  , name : Schema.User.Name
  , friends : Schema.User.Friends Friend
  }
  
type alias Friend =
  { nickname : Schema.User.Nickname
  , age : Schema.User.Age
  }

Draft

type Edge a
  = a
  | Maybe a
  | List a
  
type Friend user = Edge user

Ability to create efficient cache

One of the core advantages of GraphQL is an ability to write an efficient cache for it using denormalisation. You can simply return schema using introspection query and create caching mechanism using one.

Though it's not a prerequesite, I think we need to have this in mind while developing our GraphQL version.

-- this is result for Query1
type alias UserSlice1 =
  { id : String
  , name : String
  }

-- this is result for Query2
type alias UserSlice2 =
  { id : String
  , age : Int
  }

-- This is actual GraphQL type
type alias User =
  { id : String
  , name : String
  , age : Int
  }

In JavaScript I can write a generic merge function to update my cache based on results for different queries.

// Naive implementation
function merge(cache, response) {
  return {
    ...cache,
    ...response,
  };
}

cache = merge(cache, userSlice1);
cache = merge(cache, userSlice2);

And even if I introduce userSlice3 later I do not need to change my merge function.

But going back to Elm since we have 3 different types we cannot just merge them in a generic way. As far as I can imagine merge function would be as follows.

type QueryResponse
  = UserSlice1
  | UserSlice2
  
merge : User -> QueryResponse -> User
merge user response =
  case response of
    UserSlice1 ->
      { cache
        | id = response.id
        , name = response.name
      }
    UserSlice2 ->
      { cache
        | id = response.id
        , age = response.age
      }

As you can see every time I introduce new type or query result I need to change my merge function. Now imagine you have 10..20..100 different queries in your application.

@scrogson

This comment has been minimized.

Copy link

commented May 11, 2016

@jackalcooper

This comment has been minimized.

Copy link

commented Nov 28, 2016

Suffering from merge functions here as well!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.