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

Recursive fields: add Arg.fix and Schema.fix #199

Merged
merged 5 commits into from
Mar 29, 2021

Conversation

anmonteiro
Copy link
Contributor

fixes #134 by implementing the proposed fix approach.

@@ -2,8 +2,7 @@ let yojson =
( module struct
type t = Yojson.Basic.json

let pp formatter t =
Format.pp_print_text formatter (Yojson.Basic.pretty_to_string t)
let pp = Yojson.Basic.pretty_print ?std:None
Copy link
Contributor Author

@anmonteiro anmonteiro Feb 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by change for better diffs when testing

Copy link
Owner

@andreas andreas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @anmonteiro! Thanks for implementing this 🙂 A couple of thoughts...

I think the type recursive is unnecessarily constraining, as it does not allow mutually recursive types with differing source type parameters. I think it could work with a single type parameter 'a. About the type name, my current favorite is fixor fixpoint, as it's so closely tied to the function of the same name.

Besides those details, what's previously held me back from landing something like this, is how to have a cohesive API design that encompasses interfaces and unions. I think it would be preferable to construct recursive types with unions and interfaces in a uniform manner, i.e. add fields interface and union to the recursive/fixpoint type. This would also improve a part of the current API, which I consider to be a sore point. There are some details I haven't thought through in regards to converting to an abstract value. I'll give it a think and back to you. Feel free to also take a stab at it 🙂

@anmonteiro
Copy link
Contributor Author

@andreas Thanks for the prompt review.

I understand the limitation but I'm not sure how the type recursive can take less type parameters. Could you expand a little more on what you were thinking? I'm gonna dig into it further and try to figure it out myself anyway.

As for interfaces and unions, I agree it probably makes sense to explore its unification to this fixpoint API. I'm gonna play with it too. Do you think it makes sense to include it in this initial proposal or iterate on it over time?

@anmonteiro
Copy link
Contributor Author

I just pushed a commit that makes the fixpoint type parameterized on just 'a

@anmonteiro
Copy link
Contributor Author

commit d95b99e adds interface and union fields in the fixpoint definition too. I'm not sure if this is what you had in mind though.

@andreas
Copy link
Owner

andreas commented Feb 21, 2021

Nice, I like the fixpoint type a lot better now 🙂

I think we need to test the proposed change with an example a group of mutually recursive types that include an interface, a union or both to make sure we address that complexity. It's not clear to me that it would work out well without having investigated it further.

Currently when you mix recursion and abstract types it works, but it's quite ugly and tricky. It requires use of lazy, mutability, Schema.add_typ and forcing lazy values with unit return value. Here's an example from irmin:

  (* ... *)
  and node = Schema.union "Node"
  and tree_as_node = lazy (Schema.add_type node (Lazy.force tree))
  and contents_as_node = lazy (Schema.add_type node (Lazy.force contents))

  [@@@ocaml.warning "-5"]

  let _ = Lazy.force tree_as_node
  let _ = Lazy.force contents_as_node

Maybe you can still get away with something like the above with the new fix function, but I would like to confirm that and see how it looks.

However, the new fixpoint construction means that we could instead require members of an abstract type to be specified when it is constructed, rather than added later with Schema.add_typ. This would mean we get rid of the mutability, lazy, etc. The above example would look something like the following:

  (* ... *)
  and node, MagicList.[tree_as_node; contents_as_node] = Schema.union "Node" ~types:AnotherMagicList.[tree; contents]

The complexity is instead shifted to the return value of union and interface, which needs to return functions that can project values into the abstract source type. This is what I've tried to indicate with MagicList and AnotherMagicList above. I haven't yet tried implementing the above to understand the tradeoff better.

I hope this clarifies my thinking a bit more 🙂

@anmonteiro
Copy link
Contributor Author

Gotcha, I do understand your point now.

Some thoughts:

  • the implementation in this PR makes it easier to describe the example you provided based on Irmin.
  • agree we should probably include a test case that better exemplifies that mixing & matching of mutually-recursive GraphQL constructs
  • I think, however, requiring members of an abstract type to be specified at the time of its creation might make a little harder to generate dynamic schemas, which is currently a use case of mine.

@andreas
Copy link
Owner

andreas commented Mar 6, 2021

I've had a chance to try this out with irmin-graphql, and I think it's a net win, even with the inconsistencies for abstract types. I think union should be removed from fixpoint though, as it provides no value at this point. After that, I'm happy to merge this PR 🙂 (bonus points if you update the readme!)

I'm inclined to move towards specifying members of an abstract type at creation time as a follow-up. This would eliminate the last uses of lazy in irmin-graphql. I would appreciate if you can elaborate on your concerns though. I might have missed some limitations with this approach.

@anmonteiro
Copy link
Contributor Author

@andreas thanks for trying this out! Happy to remove union and update the readme.

my concerns around dynamic schemas exist today even with the proposed approach. Let's say you want to define a self-recursive input object type with some dynamic arguments. Right now my code defines:

type _ dynamic_args =
  | DynamicArgs :
      { adapter : 'a -> 'args
      ; args : ('a, 'args) Gql.Arg.arg_list
      }
      -> 'a dynamic_args

In the above, we're hiding the value of 'args. How do I now create such type? Using the approach proposed in this PR, I could do something like:

Gql.Arg.fix (fun recursive ->
  recursive.obj
    name
    ~fields:(fun self ->
      let (DynamicArgs { args; adapter }) = get_dynamic_args_from_somewhere in
      args)
    ~coerce:(adapter StringMap.empty))

Right now there are 2 issues I'm working around:

  1. I can't simply return args from the fields function. OCaml taps out with:
Error: This expression has type
         (Json.t StringMap.t, $DynamicArgsFields_'b1)
         Graphql_lwt.Schema.Arg.arg_list
       but an expression was expected of type
         (Json.t StringMap.t, 'a) Graphql_lwt.Schema.Arg.arg_list
       The type constructor $DynamicArgs_'b1 would escape its scope
  1. there's no way to generate an adapter based on self, as it needs to be passed in outside the fields function

Hope this is helpful to understand the issue I'm facing.

@anmonteiro
Copy link
Contributor Author

I don't necessarily see a way around the scope escape generally. I'll push a commit addressing the other suggestions shortly.

@anmonteiro
Copy link
Contributor Author

Hey @andreas, gentle ping on this one.

@andreas andreas merged commit 4e4f626 into andreas:master Mar 29, 2021
@andreas
Copy link
Owner

andreas commented Mar 29, 2021

Thanks @anmonteiro!

@anmonteiro anmonteiro deleted the anmonteiro/recursive-args-fix branch March 30, 2021 00:57
@zshipko zshipko mentioned this pull request May 24, 2022
kit-ty-kate pushed a commit to ocaml/opam-repository that referenced this pull request Jul 19, 2022
…d graphql-async (0.14.0)

CHANGES:

- Support `__typename` on subscriptions (andreas/ocaml-graphql-server#178)
- Handle unknown fields for subscriptions (andreas/ocaml-graphql-server#178)
- Add ocamlformat (andreas/ocaml-graphql-server#177)
- Handle missing variables as null (andreas/ocaml-graphql-server#184)
- Show default value in introspection query (andreas/ocaml-graphql-server#194)
- Support block strings in the parser (andreas/ocaml-graphql-server#198)
- Handle skip/include directives on fragment spreads (andreas/ocaml-graphql-server#200)
- Improved handling of recursive arguments and objects (andreas/ocaml-graphql-server#199)
- Fix websocket conflict (andreas/ocaml-graphql-server#206)
- Update deprecated Fmt functions (andreas/ocaml-graphql-server#206)
- Use Yojson `t` types instead of deprecated `json` type (andreas/ocaml-graphql-server#208)
- Raise minimum `rresult` version (andreas/ocaml-graphql-server#209)
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.

Recursive args
2 participants