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

Revert "Remove all 'instanceof GraphQLSchema' checks" #377

Merged
merged 1 commit into from
May 5, 2016

Conversation

leebyron
Copy link
Contributor

@leebyron leebyron commented May 5, 2016

Reverts #371

Now have a better understanding of why those invariants were there - because instanceof is used in many other places, removing these doesn't allow the use of GraphQLSchema defined by other versions of graphql-js because eventually checks like instanceof GraphQLList or instanceof GraphQLObjectType will fail for the same reasons - but at that point the produced errors may be far more mysterious. These invariants were added to at least give a clear error message early on.

If we want to enable this, we'll have to completely remove instanceof.

@leebyron leebyron merged commit 2ac41f6 into master May 5, 2016
@leebyron leebyron deleted the revert-371-drop-instanceof branch May 5, 2016 16:23
@helfer
Copy link
Contributor

helfer commented May 5, 2016

I see. It would have enabled modifying things that build a schema while still using the original GraphQL-JS types, but clearly it makes more sense to do this properly. I'll work on it as soon as I have time.

@helfer
Copy link
Contributor

helfer commented May 5, 2016

@leebyron I see this is going to be quite a lot of work, because instanceof is used almost ubiquitously.

I'd like to get started on this, but since it's going to be a major change, I'd like to have your thumbs up about the approach first:

I think the most straightforward way to implement all these checks is to replace the instanceof call with a function that checks if an object is of a certain type. Rather than using duck typing, I think it's worth adding a __type field to each of the types, and just checking for equivalence of that. Thoughts?

@leebyron
Copy link
Contributor Author

leebyron commented May 5, 2016

Something like that is probably right. To better match the AST maybe we call it "kind" (this also avoids objects which model types having a field called type)

One of the biggest challenges will be getting flow error checking to behave correctly. Currently it relies on instanceof checks to resolve unions

@sheerun
Copy link

sheerun commented May 5, 2016

What about:

  1. Creating interfaces for each type like GraphQLSchemaInterface
  2. Making actual classes like GraphQLSchema to inherit from interfaces
  3. Replacing instanceof checks to interfaces instead of actual classes
  4. Requiring external modules to inherit from interfaces

@leebyron
Copy link
Contributor Author

leebyron commented May 5, 2016

Can you expand on what you mean by interface?

@helfer
Copy link
Contributor

helfer commented May 5, 2016

I think by interface he means the way interfaces are used in languages like Java. Instead of having actual functionality, they define what functions and attributes implementing types need to have.

Since Javascript doesn't have a concept of interfaces, this would essentially come down to making an abstract class for each type, which has no properties, and then extending that for the actual class. That would work for writing custom types, but it still wouldn't allow you to use different versions of GraphQL-JS alongside each other, unless these 'interfaces' were broken out into a separate package and imported from there into GraphQL-JS.

@sheerun Is that an accurate description of your idea?

@leebyron
Copy link
Contributor Author

leebyron commented May 5, 2016

Yeah, since JavaScript doesn't have a concept of interfaces or abstract classes I think that's probably a non-starter for replacing instanceof.

@sheerun
Copy link

sheerun commented May 5, 2016

Yes, it's accurate. If flow only supports only instanceof then we need to deal with it.

On the other hand I'm not a fan of nominal typing, I rather prefer something like scala's traits or haskell typeclasses or go-lang interfaces. i.e. I'd be really glad if you used structural typing instead as it allows for behavior @helfer expects: implementing interface without inheriting class.

In flow realm it means using interfaces instead of implicitly defined types for classes. i.e. never write import type SomeClass, because this way you're restricting yourself to nominal type of SomeClass.

I've prepared gist to demo the idea: https://gist.github.com/sheerun/30dae1da9e74af47ce91ca19d031bfac

@leebyron
Copy link
Contributor Author

leebyron commented May 5, 2016

I don't have a problem with nominal types, but agree that structural typing is usually better and more flow encourages that.

The issue here is with replacing instanceof. Unfortunately flow is a static-time-only type checker, so we if we have a flow union type, we need some way to tell flow something about the runtime object to help it understand which type we're using.

e.g.:

class One { oneMethod() { return 'one' } }
class Two { twoMethod() { return 'two' } }
type OneOrTwo = One | Two;

function acceptsUnion(oneOrTwo: OneOrTwo) {
  if (oneOrTwo instanceof One) {
    return oneOrTwo.oneMethod();
  } else {
    return oneOrTwo.twoMethod();
  }
}

This sort of pattern shows up all the time when differentiating between GraphQLObjectType, GraphQLInterfaceType, etc.

So using flow interfaces and regular objects doesn't allow us to use this pattern anymore because we can't use the interface at runtime to disambiguate the union type.

One possible solution is to mirror how the AST works by using a kind property to implement a sort of "tagged union".

type ObjectType = { kind: 'ObjectType', fieldX: Type }
type InterfaceType =  = { kind: 'InterfaceType', fieldY: Type }
type ObjectOrInterfaceType = ObjectType | InterfaceType

function acceptsUnion(type: ObjectOrInterfaceType) {
  if (type.kind === 'ObjectType') { return type.fieldX; }
}

We'll just have to be careful to ensure flow treats this correctly and doesn't introduce any false-positives or false-negatives.

@leebyron
Copy link
Contributor Author

leebyron commented May 5, 2016

I have no idea if this will work or not, but worth a try:

class GraphQLObjectType {
  kind: ('ObjectType': 'ObjectType')
}

so we can continue to use the existing type names throughout, but tell flow that there is a property which is of the constant value type 'ObjectType'.

Maybe?

@leebyron
Copy link
Contributor Author

leebyron commented May 5, 2016

@samwgoldman ideas?

@sheerun
Copy link

sheerun commented May 6, 2016

Flow supports tagged unions but it seems they only work for type unions, and not for interface unions.

/* @flow */

type ObjectType = {
  kind: 'ObjectType',
  fieldX: string
};

type InterfaceType = {
  kind: 'InterfaceType',
  fieldY: string
};

type ObjectOrInterfaceType = ObjectType | InterfaceType;

function acceptsUnion(type: ObjectOrInterfaceType) {
  if (type.kind === 'ObjectType') {
    return 'Object: ' + type.fieldX;
  } else {
    return 'Interface: ' + type.fieldY;
  }
}

class ObjectImplementation {
  kind: 'ObjectType';
  fieldX: string;

  constructor() {
    this.kind = 'ObjectType';
    this.fieldX = 'foo';
  }
}

class Object2Implementation {
  kind: 'ObjectType';
  fieldX: string;

  constructor() {
    this.kind = 'ObjectType';
    this.fieldX = 'foo';
  }
}

class InterfaceImplementation {
  kind: 'InterfaceType';
  fieldY: string;

  constructor() {
    this.kind = 'InterfaceType';
    this.fieldY = 'bar';
  }
}

console.log(acceptsUnion(new ObjectImplementation()));
console.log(acceptsUnion(new Object2Implementation()));
console.log(acceptsUnion(new InterfaceImplementation()));

@samwgoldman
Copy link
Contributor

I am missing a lot of context here, but just looking at GraphQLSchema in isolation it looks like you could define an interface—let's call itIGraphQLSchema—of which GraphQLSchema instances would be an inhabitant. Some of the return types in methods on GraphQLSchema are also nominal types, so you'd want to make those interfaces as well.

I'm not sure how disjoint unions are related though.

sogko added a commit to sogko/graphql-js that referenced this pull request Jun 1, 2016
* master: (26 commits)
  0.6.0
  Validation: improving overlapping fields quality (graphql#386)
  Validation: context.getFragmentSpreads now accepts selectionSet rather than fragment AST node
  Factor out more closure functions
  Factor out closure functions to normal functions
  Deprecated directive (graphql#384)
  RFC: Directive location: schema definition (graphql#382)
  RFC: Schema Language Directives (graphql#376)
  Export introspection in public API
  Export directive definitions. (graphql#381)
  BUG: Ensure building AST schema does not exclude @Skip and @include (graphql#380)
  documentation of schema constructor
  Revert "Remove all 'instanceof GraphQLSchema' checks" (graphql#377)
  remove all 'instanceof GraphQLSchema' checks (graphql#371)
  Error logging for new interface type semantics (graphql#350)
  Nit: Missing case in grammar for TypeSystemDefinition in comment
  Bug: printer can print non-parsable value
  Factor out suggestion quoting utility
  Minor refactoring
  Minor refactoring of error messages for unknown fields
  ...
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

Successfully merging this pull request may close these issues.

4 participants