elm-graphql icon indicating copy to clipboard operation
elm-graphql copied to clipboard

Selecting implementation attributes from interfaces

Open Hazelfire opened this issue 3 years ago • 2 comments

This is a very early stage issue / suggestion. Feel free to ignore it for now, I'll come back to it with better details once I've verified the way we can implement it. Sorry if this is a duplicate.

In my usage of elm-graphql, I came across a use case of interfaces that doesn't seem to be covered by this library.

Say, you have a schema:

interface Node {
id : ID!
}
type Course implements Node {
 id: ID!
 name: String!
 assignments: [Assignment!]!
}
type Assignment implements Node {
  id: ID!
  name: String!
}

type Query {
  allCourses: [Course!]!
}

With this implementation, I have an interface but no field that actually references that interface. The reason I've defined this is because in Elm code, I can now do:

type Node = Node Id

nodeSelector : SelectionSet Node App.Interface.Node
nodeSelector = SelectionSet.map Node App.Interface.Node.id

And I can abstract the way that I retrieve the ID across nodes, so that in my object selection code, I have:

type Course = Course
  { id : Id,
  , name : String
  , assignments : List Assignment
  }

courseSelector : SelectionSet Course App.Objects.Course
courseSelector = SelectionSet.map3
  (\(Node id) name assignments)
  nodeSelector
  App.Objects.Course.name
  App.Objects.Course.assignments assignmentsSelector

This however fails, because nodeSelector is of type SelectionSet Node App.Interface.Node when it wants it to be SelectionSet Node App.Objects.Course.

Having this functionality, allowing you to select from an object fields from an interface it implements, seems pretty trivial, I haven't properly tested whether this would work, but because the second element in SelectionSet is just a phantom type, I'd imagine adding something similar to the following would work perfectly fine.

selectFromCourse : SelectionSet a App.Interface.Node -> SelectionSet a App.Interface.Course
selectFromCourse (SelectionSet a b) = SelectionSet a b

This is however, currently completely untested. So consider this issue to be in a draft stage. Feel free to also tell me I'm using GraphQL wrong.

Hazelfire avatar Dec 20 '21 04:12 Hazelfire

Hey @Hazelfire, thanks for the discussion. I see what you're saying there, that's an interesting point.

I think you're right that the selectFromCourse type signature would be one way to support that.

Another idea, though a much more drastic change, would be to use Records as the Phantom Types. So taking this example from the Star Wars API example, since I'm looking at this source code in the examples folder for this one:

https://github.com/dillonkearns/elm-graphql/blob/8dcf2b4fd22b11d4ebf4ce6ea96d07f794f3f741/examples/src/Swapi/Object/Human.elm#L60-L64

https://github.com/dillonkearns/elm-graphql/blob/8dcf2b4fd22b11d4ebf4ce6ea96d07f794f3f741/examples/src/Swapi/Interface/Character.elm#L81-L85

https://github.com/dillonkearns/elm-graphql/blob/8dcf2b4fd22b11d4ebf4ce6ea96d07f794f3f741/examples/src/Swapi/Query.elm#L72-L85

What if it was something like this:

-- in Swapi.Interface.Character
name : SelectionSet String { objectHuman : () }
name =
    Object.selectionForField "String" "name" [] Decode.string
-- in Swapi.Interface.Character
name : SelectionSet String { interfaceCharacter : (), objectHuman : (), objectDroid : () }
name =
    Object.selectionForField "String" "name" [] Decode.string
hero :
    (HeroOptionalArguments -> HeroOptionalArguments)
    -> SelectionSet decodesTo { character | interfaceCharacter : () }
    -> SelectionSet decodesTo RootQuery

This would allow you to have

Benefits of Record Approach

This is lightweight in a few ways.

  1. You can intermix things that need the name field as part of a Character interface, or as part of one of its Object implementors (Droid or Human)
  2. No import needed for the named types (Swapi.Interface.Character, etc.) - they are just records now, so they're structurally typed rather than nominally typed

Downsides of Record Approach

  1. Is it more cumbersome or confusing to have these records rather than named types?
  2. More type variables in a few places. Could that be confusing for some users? Could that be cumbersome at all in any cases even for users who are familiar with that technique?
  3. In an API with a ton of types, this could become unwieldy. For example, the Node type in GitHub has almost 80 different implementors: https://github.com/dillonkearns/elm-graphql/blob/8dcf2b4fd22b11d4ebf4ce6ea96d07f794f3f741/examples/src/Github/Interface/Node.elm#L23-L102 - these could be given type aliases easily, so maybe that would be fine?

Thoughts

Anyway, just throwing an idea out there, still need to think on it a little to decide if it's good or not. But seems like it might be worth considering that direction more seriously.

dillonkearns avatar Dec 20 '21 05:12 dillonkearns

I've come back to this several times, trying to understand what you mean and how it could be implemented. The fact that I still don't understand it is a bit of a concern.

I think I get the general idea, but I do think that it might be a bit too on the nose of what's possible, and a much simpler implementation with much more obvious ways to do this might be preferable, at least in my eyes.

Hazelfire avatar Dec 22 '21 00:12 Hazelfire