Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selecting implementation attributes from interfaces #577

Open
Hazelfire opened this issue Dec 20, 2021 · 2 comments
Open

Selecting implementation attributes from interfaces #577

Hazelfire opened this issue Dec 20, 2021 · 2 comments

Comments

@Hazelfire
Copy link

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.

@dillonkearns
Copy link
Owner

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:

{-| The name of the human.
-}
name : SelectionSet String Swapi.Object.Human
name =
Object.selectionForField "String" "name" [] Decode.string

{-| The name of the character.
-}
name : SelectionSet String Swapi.Interface.Character
name =
Object.selectionForField "String" "name" [] Decode.string

hero :
(HeroOptionalArguments -> HeroOptionalArguments)
-> SelectionSet decodesTo Swapi.Interface.Character
-> SelectionSet decodesTo RootQuery
hero fillInOptionals____ object____ =
let
filledInOptionals____ =
fillInOptionals____ { episode = Absent }
optionalArgs____ =
[ Argument.optional "episode" filledInOptionals____.episode (Encode.enum Swapi.Enum.Episode.toString) ]
|> List.filterMap Basics.identity
in
Object.selectionForCompositeField "hero" optionalArgs____ object____ Basics.identity

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:
    { onAddedToProjectEvent : SelectionSet decodesTo Github.Object.AddedToProjectEvent
    , onAssignedEvent : SelectionSet decodesTo Github.Object.AssignedEvent
    , onBaseRefChangedEvent : SelectionSet decodesTo Github.Object.BaseRefChangedEvent
    , onBaseRefForcePushedEvent : SelectionSet decodesTo Github.Object.BaseRefForcePushedEvent
    , onBlob : SelectionSet decodesTo Github.Object.Blob
    , onBot : SelectionSet decodesTo Github.Object.Bot
    , onClosedEvent : SelectionSet decodesTo Github.Object.ClosedEvent
    , onCommentDeletedEvent : SelectionSet decodesTo Github.Object.CommentDeletedEvent
    , onCommit : SelectionSet decodesTo Github.Object.Commit
    , onCommitComment : SelectionSet decodesTo Github.Object.CommitComment
    , onCommitCommentThread : SelectionSet decodesTo Github.Object.CommitCommentThread
    , onConvertedNoteToIssueEvent : SelectionSet decodesTo Github.Object.ConvertedNoteToIssueEvent
    , onCrossReferencedEvent : SelectionSet decodesTo Github.Object.CrossReferencedEvent
    , onDemilestonedEvent : SelectionSet decodesTo Github.Object.DemilestonedEvent
    , onDeployKey : SelectionSet decodesTo Github.Object.DeployKey
    , onDeployedEvent : SelectionSet decodesTo Github.Object.DeployedEvent
    , onDeployment : SelectionSet decodesTo Github.Object.Deployment
    , onDeploymentStatus : SelectionSet decodesTo Github.Object.DeploymentStatus
    , onExternalIdentity : SelectionSet decodesTo Github.Object.ExternalIdentity
    , onGist : SelectionSet decodesTo Github.Object.Gist
    , onGistComment : SelectionSet decodesTo Github.Object.GistComment
    , onHeadRefDeletedEvent : SelectionSet decodesTo Github.Object.HeadRefDeletedEvent
    , onHeadRefForcePushedEvent : SelectionSet decodesTo Github.Object.HeadRefForcePushedEvent
    , onHeadRefRestoredEvent : SelectionSet decodesTo Github.Object.HeadRefRestoredEvent
    , onIssue : SelectionSet decodesTo Github.Object.Issue
    , onIssueComment : SelectionSet decodesTo Github.Object.IssueComment
    , onLabel : SelectionSet decodesTo Github.Object.Label
    , onLabeledEvent : SelectionSet decodesTo Github.Object.LabeledEvent
    , onLanguage : SelectionSet decodesTo Github.Object.Language
    , onLockedEvent : SelectionSet decodesTo Github.Object.LockedEvent
    , onMarketplaceListing : SelectionSet decodesTo Github.Object.MarketplaceListing
    , onMentionedEvent : SelectionSet decodesTo Github.Object.MentionedEvent
    , onMergedEvent : SelectionSet decodesTo Github.Object.MergedEvent
    , onMilestone : SelectionSet decodesTo Github.Object.Milestone
    , onMilestonedEvent : SelectionSet decodesTo Github.Object.MilestonedEvent
    , onMovedColumnsInProjectEvent : SelectionSet decodesTo Github.Object.MovedColumnsInProjectEvent
    , onOrganization : SelectionSet decodesTo Github.Object.Organization
    , onOrganizationIdentityProvider : SelectionSet decodesTo Github.Object.OrganizationIdentityProvider
    , onOrganizationInvitation : SelectionSet decodesTo Github.Object.OrganizationInvitation
    , onProject : SelectionSet decodesTo Github.Object.Project
    , onProjectCard : SelectionSet decodesTo Github.Object.ProjectCard
    , onProjectColumn : SelectionSet decodesTo Github.Object.ProjectColumn
    , onProtectedBranch : SelectionSet decodesTo Github.Object.ProtectedBranch
    , onPublicKey : SelectionSet decodesTo Github.Object.PublicKey
    , onPullRequest : SelectionSet decodesTo Github.Object.PullRequest
    , onPullRequestCommit : SelectionSet decodesTo Github.Object.PullRequestCommit
    , onPullRequestReview : SelectionSet decodesTo Github.Object.PullRequestReview
    , onPullRequestReviewComment : SelectionSet decodesTo Github.Object.PullRequestReviewComment
    , onPullRequestReviewThread : SelectionSet decodesTo Github.Object.PullRequestReviewThread
    , onPushAllowance : SelectionSet decodesTo Github.Object.PushAllowance
    , onReaction : SelectionSet decodesTo Github.Object.Reaction
    , onRef : SelectionSet decodesTo Github.Object.Ref
    , onReferencedEvent : SelectionSet decodesTo Github.Object.ReferencedEvent
    , onRelease : SelectionSet decodesTo Github.Object.Release
    , onReleaseAsset : SelectionSet decodesTo Github.Object.ReleaseAsset
    , onRemovedFromProjectEvent : SelectionSet decodesTo Github.Object.RemovedFromProjectEvent
    , onRenamedTitleEvent : SelectionSet decodesTo Github.Object.RenamedTitleEvent
    , onReopenedEvent : SelectionSet decodesTo Github.Object.ReopenedEvent
    , onRepository : SelectionSet decodesTo Github.Object.Repository
    , onRepositoryInvitation : SelectionSet decodesTo Github.Object.RepositoryInvitation
    , onRepositoryTopic : SelectionSet decodesTo Github.Object.RepositoryTopic
    , onReviewDismissalAllowance : SelectionSet decodesTo Github.Object.ReviewDismissalAllowance
    , onReviewDismissedEvent : SelectionSet decodesTo Github.Object.ReviewDismissedEvent
    , onReviewRequest : SelectionSet decodesTo Github.Object.ReviewRequest
    , onReviewRequestRemovedEvent : SelectionSet decodesTo Github.Object.ReviewRequestRemovedEvent
    , onReviewRequestedEvent : SelectionSet decodesTo Github.Object.ReviewRequestedEvent
    , onStatus : SelectionSet decodesTo Github.Object.Status
    , onStatusContext : SelectionSet decodesTo Github.Object.StatusContext
    , onSubscribedEvent : SelectionSet decodesTo Github.Object.SubscribedEvent
    , onTag : SelectionSet decodesTo Github.Object.Tag
    , onTeam : SelectionSet decodesTo Github.Object.Team
    , onTopic : SelectionSet decodesTo Github.Object.Topic
    , onTree : SelectionSet decodesTo Github.Object.Tree
    , onUnassignedEvent : SelectionSet decodesTo Github.Object.UnassignedEvent
    , onUnlabeledEvent : SelectionSet decodesTo Github.Object.UnlabeledEvent
    , onUnlockedEvent : SelectionSet decodesTo Github.Object.UnlockedEvent
    , onUnsubscribedEvent : SelectionSet decodesTo Github.Object.UnsubscribedEvent
    , onUser : SelectionSet decodesTo Github.Object.User
    , onUserContentEdit : SelectionSet decodesTo Github.Object.UserContentEdit
    }
    - 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.

@Hazelfire
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants