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

Subscription and useSubscription API discussion. #72

Closed
parkerziegler opened this issue Jun 20, 2019 · 2 comments
Closed

Subscription and useSubscription API discussion. #72

parkerziegler opened this issue Jun 20, 2019 · 2 comments
Labels
enhancement New feature or request

Comments

@parkerziegler
Copy link
Contributor

@Schmavery @gugahoa I'm opening this issue here as a place for us to track API ideas for Subscription and useSubscription. I checked out the implementation here: https://reasonml.github.io/en/try?rrjsx=true&reason=C4TwDgpgBAThCOBXCBnYAKA5AIwJRQF4AoKKAHygCEB7ADyzwG4ijRIoALAQwDsATADYQYWLgBooOCZgDG+YqQoAJXoIjp01MMACW1HqNzS8hAHySuuAFydVQkVt37Dx6ZZLkoAOWor+Qm25-YXQAfVdJJhY2aEQUCABlRGwUGRgdbT0DTHFI6TlCD3QAPyC1GEC7EJyI2SMoYrgkVGAbJuQ0Blx5c1lmIiFgKAAzampC0nQYqFzp7AlpmQlSqorbYJFc+ag5ZfaWtoQOjDxrHbMoAG8PUkGqOnQYeVgjluZSUhQAdx1gGQ4oOgyvZ8NcPp4-GogatQsMeD11uVYQYfDwIBInjdPD5IUILjAsQBfd5QYkeIijaglYHCAg4qp7V5oAg0egAVm6zEp1NWdN8DIa+2ZrPQACI2aLORSxjyNgRcep0FwZEsoHwuMBLBcwVBvr9-oDlQUdRRUdACOYANrqzUAXQ8FAS1AAtuoBAiBFAAAJQa0arj20iEoiE+qNJnAFkPDm4RhAA and I'll confess I don't 100% understand it, but it seems like an interesting idea. I guess the major discussions to have are:

  1. Do we want to consolidate the API surface into 1 hook / 1 component or do 2 hooks / 2 components to handle handler?
  2. If we go for 1 hook / 1 component can we simplify @gugahoa's implementation in any way or use @Schmavery's idea of a default handler that'd behave like (~prevSubscriptions as _, ~subscription) => subscription?
  3. If we go for 2 hooks / 2 components, can we find a nice way to consolidate the shared pieces?

I'd say let's discuss things here and folks can feel free to submit PRs w/ suggested implementations. Thanks to you both so much for all these contributions too, I can really feel the lib getting better and better.

@gugahoa
Copy link
Contributor

gugahoa commented Jun 21, 2019

I wrote an extensive explanation on how GADT can solve the problem with we go with 1 hook / 1 component, I believe it can't be further simplified, but all that complexity will only live inside the UrqlUseSubscription module.
I'll make a PR with this proposal so that we have a more concrete example to refer to.


For future readers without the complete context:
What we want to achieve with a single useSubscription for both cases is for it to either return an UrqlType.response('resp) or an UrqlType.response('acc) depending on the handler passed (If none, UrqlType.response('resp), otherwise UrqlType.response('acc)). The 'resp type and 'acc type come from different sources, for example:

/* handler is a function that receive previous subscription data, current data and return an accumulator of those. The accumulator can be any data structure of the user choosing. */
type handler = (option('acc), 'resp) => 'acc;

/* request represents a graphql request that returns a data with type 'a*/
type request('resp);

/* This would be useSubscription hook type definition, in pseudo-reason */
type useSubscription = (~handler: handler('acc, 'resp)=?, request('resp)) => ?;

So, the question is: which type should go on ?.
The answer is: it depends! From a first glance this can't be solved trivially, but ReasonML has a "feature" called GADT (Generalized Algebraic Data Types) (for more info, see here) which enable us to return different types depending on the function arguments.

GADT

Taking the example from the link above, how can we write a function that given the value of prim, returns the primitive value it holds?

type prim =
| Int(int)
| Float(float)
| String(string)
| Bool(bool);

If we try to write it like:

/*
  * What's the signature here?
  * let prim: prim => ?
*/
let prim =
  fun
  | Int(i) => i
  | Float(f) => f
  | String(s) => s
  | Bool(b) => b;

It won't compile with the following error:
Error: This expression has type float but an expression was expected of type int

To make it compile, we have to encode more information in our type, this can be done using what's called Phantom Type

Using a phantom type we define prim as:

type prim('a) =
| Int(int): prim(int)
| Float(float): prim(float)
| String(sring): prim(string)
| Bool(bool): prim(bool);

Here we defined a phaton type 'a for prim, and for each variant we gave it a concrete type.
Now we can use it in our eval definition:

let eval: type a. prim(a) => a =
  fun
  | Int(i) => i
  | Float(f) => f
  | String(s) => s
  | Bool(b) => b;

Here we use the phantom type in prim('a) to define our return value. Int(int) has type prim(int), Float(float) has type prim(float), and that's how we achieved polymorphic return in ReasonML.

Using GADT for useSubscription

To achieve what we want with GADT, a proof of concept (without interop) could be defined as follow:

/* Equivalent to UrqlTypes.request */
type request('resp) =
  | Box('resp);

/* 
  * A new definition of UrqlTypes.handler
  * 'ret here is our phantom type
  * 'ret is used to define our return type in useSubscription
  * When we have a Handle, 'ret is given the concrete type 'acc
  * When we have a NoHandle, 'ret is given the concrete type 'resp
*/
type handler('acc, 'resp, 'ret) =
  | Handle((option('acc), 'resp) => 'acc): handler(option('acc), 'resp, 'acc)
  | NoHandle: handler(_, 'resp, 'resp);

/* Type definition of useSubscription hook */
type useSubscription('acc, 'resp, 'ret) =
  (~handler: handler('acc, 'resp, 'ret), ~request: request('resp)) => 'ret;

/* Actual hook */
/*
 * This signature reads as:
 * For all types acc, resp and ret
 * We receive a named argument of type handler(acc, resp, ret)
 * And a request of type request(resp)
 * The return is of type ret
 */
let hook =
  (type acc, type resp, type ret, ~handler: handler(acc, resp, ret), ~request: request(resp)): ret => {
    let Box(r) = request;
    switch (handler) {
    | Handle(handler_fn) => handler_fn(None, r)
    | NoHandle => r
    };
  };

let my_handler = (acc, data) => {
  switch (acc) {
  | None => [data]
  | Some(l) => l @ [data]
  }
};

foo(~handler=NoHandle, ~request=Box(5));
/* Returns int 5 */
foo(~handler=NoHandle, ~request=Box("5"));
/* Returns string "5" */
foo(~handler=Handle(my_handler), ~request=Box(5));
/* Returns list(int) [5] */
foo(~handler=Handle(my_handler), ~request=Box("5"));
/* Returns list(string) ["5"] */

@parkerziegler
Copy link
Contributor Author

Closing for now, with the current approach being a 1 hook, 2 component solution 😂 We're using the GADT approach for the hook, and will port it over once the @react.component PPX supports locally abstract types necessary for GADTs to work.

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

No branches or pull requests

2 participants