Background
One of the biggest causes of confusion for users of dillonkearns/elm-graphql
is the syntax for optional arguments. In fact, there is a blog post dedicated to teaching people how to understand and use optional arguments in dillonkearns/elm-graphql
.
I’ll use specific examples from dillonkearns/elm-graphql
in this post since that’s the context where I have the most experience with this problem. Specifically, I’ll use a somewhat simplified version for querying search
in the Github GraphQL API. Here’s an example using the search
endpoint with raw GraphQL, to make this post more concrete (you can run the query live here):
query {
search(type: REPOSITORY, query: "language: Elm") {
repositoryCount
}
}
The Problem
Because there is no way to do a record with some fields missing in Elm, there are two ways to do default record values:
- Have a record with default values populated in each field. Then use the record update syntax to change these values.
type alias SearchOptionalArguments =
{ first : Maybe Int
, after : Maybe String
, last : Maybe Int
, before : Maybe String
}
searchOptionalDefaults : SearchOptionalArguments
searchOptionalDefaults =
{ first = Nothing, after = Nothing, last = Nothing, before = Nothing }
In dillonkearns/elm-graphql
, code is generated for these defaults, but for other use cases it would have to be hand coded.
The two pain points with this syntax are 1) you have to import the right type and reference it like { MyModule.defaults | episode = NewHope }
. I think it would be confusing and cumbersome for users to have to know to look for a record that is provided that is of the right record type (there are a lot of record types like this in dillonkearns/elm-graphql
generated code, one for each set of optional arguments in a GraphQL API) 2) you currently can’t have a qualified reference in record update syntax, so you actually have to do this workaround
let
defaults = Github.Query.searchOptionalDefaults
in
{ defaults | episode = NewHope }
This update syntax limitation is being tracked in this issue under “Record Suggestions”.
- This is the approach the
dillonkearns/elm-graphql
currently uses. Build
a function which passes the default value to the user, allowing them to
use the record update syntax to add optional fields:
Github.Query.search (\optionals -> { optionals | first = Just 100 }) { query = "Elm" }
Or use identity
to add no optional fields.
Github.Query.search identity { query = "Elm" }
This solves the decoupling problem above, but has its own complexity. This trips a lot of people up. It can be challenging to understand how to use it and read the type signatures. Understanding how and why to use identity
when you don’t want any optional arguments is confusing. And this also requires the optional and required arguments to be passed in separately ({ query = "Elm" }
is the required argument in this example).
When I present type-safe GraphQL code in Elm as a cool example of the power of Elm, this is a detail I go to lengths to try to hide to avoid confusion.
A Possible Improvement
I think that an Optional Key Record syntax could improve this quite a bit. The syntax for creating Optional Key Records could look similar to an extensible record annotation.
So this code (without Optional Key Records):
Github.Query.search (\optionals -> { optionals | first = Just 100 }) { query = "Elm" }
Would turn into this (with Optional Key Records):
Github.Query.search { ? | first = Just 100, query = "Elm" }
The { ? | ... }
explicitly indicates that missing values are filled in with Nothing
. Type annotations for such records could use a similar syntax if they wanted to omit some of the Maybe
fields in the record from the annotation, even though they might be passed to a function that depends on that Maybe
field.
For example,
addLanguageToQuery : { searchArguments? | query : String } -> { searchArguments? | query : String }
addLanguageToQuery searchArguments =
{ searchArguments? | query = searchArguments.query ++ " language: Elm" }
For reference, the full searchArguments
type without an Optional Extensible Record notation would look like this:
type alias SearchArguments =
{ first : Maybe Int
, after : Maybe String
, last : Maybe Int
, before : Maybe String
, query : String
}
I think that users would find writing their GraphQL queries in Elm much easier with this change. It would also make the GraphQL query code much more consistent and predictable (instead of some functions taking an optional arguments function, some taking a required arguments record, or a permutation of the two).
I’m curious to hear if there are other use cases that could be improved by an Optional Key Record syntax. And I’d love to hear thoughts on the idea or alternatives to the syntax. Thanks!