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

Support for ? operator for option/result types #1928

Open
sgrove opened this issue May 15, 2018 · 17 comments
Open

Support for ? operator for option/result types #1928

sgrove opened this issue May 15, 2018 · 17 comments
Labels
Parser parsing reason code into an AST RFC For larger proposals/ideas that will need discussion

Comments

@sgrove
Copy link
Contributor

sgrove commented May 15, 2018

Dealing with deeply nested, highly optional data structures (very common in real-world GraphQL where everything is nullable by default) is fairly tedious right now, and the feedback from the workshop was that having to use switch at each subsequent level was very cumbersome, and a custom infix operator was a bit mystical if it was defined inline and then refmt adds a lot of whitespace around it.

It might be nice to adopt Rust's ? operator, possibly even as a simple syntax expansion.

The goal would be to turn something like this (real-world example):

let make = _children => {
  render: (_) => {
    let reposQuery = GetRepositories.make(~name="sgrove", ());
    <GetRepositoriesQuery variables=resposQuery##variables>
      ...(
           ({result}) =>
             switch result {
             | NoData => <div> (ReasonReact.string("No Data")) </div>
             | Loading => <div> (ReasonReact.string("Loading")) </div>
             | Error(error) => <div> (ReasonReact.string(error)) </div>
             | Data(response) =>
               <div>
                 (
                   switch result##viewer {
                   | None => ReasonReact.string("No viewer")
                   | Some(viewer) =>
                     switch viewer##repositories {
                     | None => ReasonReact.string("No repos")
                     | Some(repositories) =>
                       switch repositories##edges##nodes {
                       | None => ReasonReact.string("No nodes")
                       | Some(nodes) =>
                         ReasonReact.array(
                           Array.map((
                             node =>
                               switch node {
                               | None => ReasonReact.string("Null repo")
                               | Some(node) =>
                                 switch node##name {
                                 | None => ReasonReact.string("No repo name")
                                 | Some(name) => ReasonReact.string(name)
                                 }
                               },
                             nodes
                           ))
                         )
                       }
                     }
                   }
                 )
               </div>
             }
         )
    </GetRepositoriesQuery>;
  }
}

Into this:

let make = _children => {
  render: (_) => {
    let reposQuery = GetRepositories.make(~name="sgrove", ());
    <GetRepositoriesQuery variables=resposQuery.variables>
      ...(
           ({result}) =>
             switch result {
             | NoData => <div> (string("No Data")) </div>
             | Loading => <div> (string("Loading")) </div>
             | Error(error) => <div> (string(error)) </div>
             | Data(response) =>
               <div>
                 (
                   switch result##viewer?##repositories?##edges##nodes {
                   | None => nullElement
                   | Some(nodes) => array(
                     Array##map((node =>
                         switch node?##name {
                         | None => string("Null repo or name")
                         | Some(name) => string(name)
                         },
                       nodes
                     ))
                   )
                 })
               </div>
             }
         )
    </GetRepositoriesQuery>;
  }
};

Rust's operator works on both Result and Option types which would likely be difficult, and Reason also has record-field-access and object-field-access syntax, so there's plenty to consider when introducing syntax support for this.

@cristianoc
Copy link
Contributor

So it looks like the desired effect is a more compact form of the following use of accessor functions.

let viewer = x => x#viewer;

let repositories = x => x#repositories;

let edges = x => x#edges;

let nodes = x => x#nodes;

let (|>?) = (x, access) =>
  switch (x) {
  | None => None
  | Some(v) => access(v)
  };

let (|>+) = (x, access) =>
  switch (x) {
  | None => None
  | Some(v) => Some(access(v))
  };

let test = x => Some(x) |>? viewer |>? repositories |>+ edges |>? nodes;

@fakenickels
Copy link

Currently what I'm using to do this is to use a bind operator,

result##viewer >>= (viewer => viewer##repositories) >>= (repositories => repositories##edges) >>= (edges => edges##nodes)

But stills a bit overwhelming to write. Would be nice to be able to have some kind to have a more succinct way to write that.

Maybe this is more a bucklescript's concern.

@hcarty
Copy link
Contributor

hcarty commented May 15, 2018

If you don't mind using a ppx syntax extension, both ocaml-monadic and ppx_let provide some shorthand syntax for let bindings.

I'm not aware of anything supporting this now, but ## is handled as a special case in syntax extensions. It should be possible to provide some basic syntax using ##?, for example, as a shorthand for >>= but acting like ## with respect to precedence. That could be a useful way to experiment with the idea without having to hack on Reason itself.

@jordwalke
Copy link
Member

jordwalke commented May 16, 2018

@cristianoc was the |>+ operator necessary?

I believe there's a JS proposal for record.?field. It could make sense to add x##?y and obj#?method and the same time.

@cristianoc
Copy link
Contributor

@jordwalke the |>+ operator was necessary because edges is not optional.

@guilhermedecampo
Copy link

Would be awesome to add x##?y and obj#?method!

@Gregoirevda
Copy link
Contributor

I'm looking into this since the GraphQL workshop @sgrove.
I'm planning on making a PR in graphql_ppx to generate getters for each field so you can:

let || = (result, default_) => 
switch result {
  | Some(result) => result
  | None => default_
};

let nodes = (result |. viewer |. repositories |. edges |. nodes) |•| [||];

Since we know if each field is optional or not, it would avoid all user defined getters (pretty long) and the |>+handling too.

I was wondering what the impact would be on the JS bundle size. Aren't bs.deriving abstract generated getters and setters increasing your JS bundle a lot?

@chenglou
Copy link
Member

chenglou commented May 28, 2018

@Gregoirevda you probably know now, but I'll write it here for others: bs.deriving abstract doesn't generate any noisy output (it uses the equivalent of bs.get/set under the hood, and all externals are erased, so yeah)

@cloudkite
Copy link

Here is the Javascript Proposal which is using the ?. operator (swift also uses ?.). So for symmetry maybe ?# and ?## would make sense? rather than #? and ##?

@cristianoc
Copy link
Contributor

Looking at the swift version, but others as well, it seems that _ ?. fld has both of these types:

option(t) => option(t)
t => option(t)

so strictly speaking it's an extension of the language. This means one needs to reach a little deep to implement it, and can't just be a JSX transform.

Also wondering what this means for type inference, and principal types in particular.
Also not clear what it should do on nested optionals, there seems to be some ambiguity there.

@cloudkite
Copy link

cloudkite commented Jun 7, 2018

@cristianoc I have no idea if this is feasible but could it be transformed into a switch?

For example given the following types:

type c = { thing: string };
type b = { c: option(c) };
type a = { b: option(b) };
let a = { b: Some({ c: Some({ thing: "hello" }) }) };

When you write an optional chained expression ie:

let z = a?.b?.c.thing

Reason would transform this to a switch:

let z = switch (a.b) {
| None => None
| Some(b) => 
    switch (b.c) {
    | None => None
    | Some(c) => Some(c.thing)
    }
};

This would be great for accessing properties on deeply optional records.

@cloudkite
Copy link

Also feel like this should only work on optional types rather than with the result type as well. Which I'm guessing would make it easier to make this a syntax expansion? This also aligns with the behaviour in swift and js.

@Gregoirevda
Copy link
Contributor

I started implementing this with the help of @IwanKaramazow

@cloudkite
Copy link

@Gregoirevda Awesome!! This is probably a seperate change but could be worth keeping in mind.
https://github.com/tc39/proposal-nullish-coalescing and its also implemented in swift http:https://www.codingexplorer.com/nil-coalescing-swift/

@chenglou
Copy link
Member

chenglou commented Jun 7, 2018

@cristianoc we'll only allow it for optionals. Allowing for non-optionals makes loose guess-based usages too easy, e.g. "yeah why not, let's just use it here".

Also, ?## isn't easily doable because "hash something" is actually part of the language's sugar for custom extension. So ##? is easy but ?## means adding a new syntax to Reason (that's incompatible with OCaml usage of BuckleScript). We should probably postpone adding this. There are other concerns I'm thinking about.

?. is too bad because I was thinking we could support trailing question mark for variable names. But I guess foo?.bar is better than foo.?bar because the former indicates that foo is the optional one. On second thought, we might still able to support foo? variable name anyway.

The other concern I have is the coding pattern where folks do:

let a = foo?.bar?.baz;
let b = foo?.bar;
...

Aka repeatedly accessing a long chain. But maybe it won't be prevalent.

Implementation-wise, I'm thinking that we should turn ?. into a first-class infix operator that desugars to @cristianoc's |>? (aka map). Visually, the ?. operator won't need spaces around it.

We don't need the version that wraps the value into a Some for now (aka bind) for now.

@cristianoc
Copy link
Contributor

Here's a test that a proposal should pass:

  type t = { a : int, b : option(t) };
  let test = (x:t) => x.b.b.a;

for appropriate replacements of . with whatever operator is used.

@jaredly jaredly added KIND: FEATURE REQUEST Parser parsing reason code into an AST RFC For larger proposals/ideas that will need discussion and removed KIND: FEATURE REQUEST labels Jun 14, 2018
@cristianoc
Copy link
Contributor

Here's an experiment for auto-unwrapping options when accessing fields: cristianoc/ocaml#1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Parser parsing reason code into an AST RFC For larger proposals/ideas that will need discussion
Projects
None yet
Development

No branches or pull requests

10 participants