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

Mutually-recursive object types/schemas #33

Closed
sgrove opened this issue Mar 2, 2017 · 8 comments
Closed

Mutually-recursive object types/schemas #33

sgrove opened this issue Mar 2, 2017 · 8 comments

Comments

@sgrove
Copy link

sgrove commented Mar 2, 2017

Thanks again for such a fun project to play with!

Related to #28, I've run into problems with mutually recursive types.

As a simplified example:

type tweet = {
  id : int;
  replies : tweet list;
  user: user;
} and user = {
    username: string;
    tweets: tweet list;
  };
  

let rec tweet = Schema.(obj
  ~name:"tweet"
  ~fields:(fun tweet -> [ (* <-- no way to reference user *)
    field "id"
      ~typ:(non_null int)
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.id)
    ;
    field "replies"
      ~typ:(non_null (list tweet))
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.replies)
    ;
    field "user"
      ~typ:(non_null user)
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.user)
      ]))
and user = Schema.(obj
  ~name:"user"
  ~fields:(fun user -> [ (* <-- no way to reference tweet *)
    field "username"
      ~typ:(non_null string)
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.id)
    ;
    field "tweets"
      ~typ:(non_null (list (non_null tweet)))
      ~args:Arg.[]
      ~resolver:(fun ctx t -> u.tweets)
  ]))

This will run into the same issues as #27, Error: This kind of expression is not allowed as right-hand side of 'let rec'

This is actually a common pattern with Relay/Apollo clients, where relationships have edges fields, and each inside that is a node field that in this case would be the actual tweet. The intermediate fields allow for e.g. cursors, total-count, etc. to describe the collection rather than the specific node.

@andreas
Copy link
Owner

andreas commented Mar 3, 2017

I'll try to see if I can come up with a good solution for this issue -- in the short term you can use the following trick with lazy:

type tweet = {
  id : int;
  replies : tweet list;
  user: user;
} and user = {
    username: string;
    tweets: tweet list;
  };
  

let rec tweet = lazy Schema.(obj
  ~name:"tweet"
  ~fields:(fun tweet -> [
    field "id"
      ~typ:(non_null int)
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.id)
    ;
    field "replies"
      ~typ:(non_null (list tweet))
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.replies)
    ;
    field "user"
      ~typ:(non_null Lazy.(force user))
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.user)
      ]))
and user = lazy Schema.(obj
  ~name:"user"
  ~fields:(fun user -> [
    field "username"
      ~typ:(non_null string)
      ~args:Arg.[]
      ~resolver:(fun ctx t -> t.id)
    ;
    field "tweets"
      ~typ:(non_null (list (non_null Lazy.(force tweet))))
      ~args:Arg.[]
      ~resolver:(fun ctx t -> u.tweets)
  ]))

@andreas
Copy link
Owner

andreas commented Mar 12, 2017

Does the above work as a solution for now?

@sgrove
Copy link
Author

sgrove commented Mar 12, 2017 via email

@andreas
Copy link
Owner

andreas commented May 25, 2017

I have a branch locally which implements a fixpoint. It permits code like this:

type foo = {
  b : bar
}
and bar = {
  f : foo
}

let foo, bar = Schema.(fix (fun factory ->
  factory.obj "foo"
    ~fields:(fun (foo, bar) -> [
      field "b"
        ~typ:bar
        ~args:Arg.[]
        ~resolve:(fun () f -> Some f.b)
    ]),
  factory.obj "bar"
    ~fields:(fun (foo, bar) -> [
      field "f"
        ~typ:foo
        ~args:Arg.[]
        ~resolve:(fun () b -> Some b.f)
    ])
))

So you get rid of lazy, but there's still some complexity involved. The types look like this:

  type 'a factory = {
    obj : 'ctx 'src. ?doc:string ->
                     string ->
                     fields:('a -> ('ctx, 'src) field list) ->
                     ('ctx, 'src option) typ
  }

  val fix : ('a factory -> 'a) -> 'a

Any thoughts on this API, @sgrove?

(note that I'm not particularly fond of factory -- it's a temporary term)

@sgrove
Copy link
Author

sgrove commented Jul 30, 2017

@andreas I'm not particular worries about how nice the API is right now (we can always refactor our client code later), but the biggest concern I have (that we're hitting in the real world right now) is circular deps, where one module can link into another (and vice-versa). I think this API would have the same problem.

Would you recommend mutually recursive modules to get around this, or do you see a better/cleaner solution?

@andreas
Copy link
Owner

andreas commented Jul 30, 2017

the biggest concern I have (that we're hitting in the real world right now) is circular deps, where one module can link into another (and vice-versa). I think this API would have the same problem.

I'm not sure I understand. Can you elaborate what you mean by circular deps with modules, perhaps with an example?

@sgrove
Copy link
Author

sgrove commented Jul 31, 2017

Sure!

So in file X I have a resolver which returns a typ with a resolver in file Y. This works fine, or vice versa.

But if file X has a resolver with a typ depending on the resolver in file Y, and somewhere in the sub-tree one of the resolvers in file Y has a typ with a resolver in file X, we have some trouble.

Imagine a Video module, that has a resolver returning Comments (in another file), which has a field urls (with all of the urls mentioned in the comment), which are returned as an interface (unrecognized urls are simply Url, others might be returned as VideoUrl if the url is recognized as pointing to a Video), and the VideoUrl now has a field video that points back to the Video module.

@sgrove sgrove mentioned this issue Jul 31, 2017
@andreas
Copy link
Owner

andreas commented Aug 2, 2017

It sounds to me like that problem is rooted in the requirement to have a non-cyclic dependency graph in OCaml. As such, it's hard to address in ocaml-graphql-server. If you want to have Video and Comment in separate files, then you probably need to use functors and then compose them in a separate file using recursive modules.

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

3 participants