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

Safe call operators ?. #2142

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

Gregoirevda
Copy link
Contributor

This PR adds support for ?., ?#, ?.+ and ?#+
Useful for accessing deeply nested properties.

Instead of writing

let userName = switch response {
  | None => None
  | Some(response) => switch response.user {
    | None => None
    | Some(user) => switch user.name {
      | None => None
      | Some(name) => Some(name)
    }
  }
};

You can write

let userName = response?.+user?.name;

Same goes for accessing Javascript objects with ?# and ?#+.

Note the + for record and Javascript objects access operators representing a bind.

?. represents a map:

type a = option({
  b: string
});

let res = switch a {
  | None => None
  | Some(a) => Some(a.b) 
};

?.+ represents a bind

type a = option({
  b: option(string)
});

let res = switch a {
  | None => None
  | Some(a) => a.b /* <-- Not elevated */
};

If the field is optional, use ?. otherwise use ?.+

Note on token associativity:
You would think ?. is left associative (a?.b)?.c, but since ?. is not runtime execution but sugar for a switch, it has to be right associative to construct itself from the deeply nested part to its parent.

Right associativity: (current)

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

left associativity

switch(
switch a  {
  | None => None
  | Some(a) =>  Some(a.b)
}) {
    | None => None
    | Some(b) => Some(b.c)
}

TODO:

  • tests
  • Define bind infix operator syntax (not sure the + is appropriate)
  • print switch back into ?. for remft in reason_pprint_ast

Linked issue: #1928

@thangngoc89
Copy link
Contributor

I love this feature. I have a lot of nested switch statement like this when working with graphql.

@hcarty
Copy link
Contributor

hcarty commented Aug 14, 2018

This is specific to option values, correct? It would be nice for new syntax to have some means of broader type support, so that at least result could be handled.

@jaredly
Copy link
Contributor

jaredly commented Aug 14, 2018

I like the idea of optional-access operators, but I'm not super happy about the syntax :/ feels pretty obfuscating, hard to google, etc.
I'm also not sure that record access is the most common use case that we should be optimising for.

What if we instead extended _ anonymous functions to include attribute access? then your example would become:

open Option; // assuming it gives us map, bind
let userName = response->bind(_.user)->bind(_.name)

Which, though a little less terse, I think is more understandable / googleable / cmd+clickable for newcomers.

Also, in the context of the let!opt PR there's a nice unification, in that you'd be doing

let!opt userName = response->opt(_.user)->opt(_.name);
"Hello " ++ userName

it's all the same function you're calling.

@Gregoirevda
Copy link
Contributor Author

Most Javascript developers know Typescripts ?.. It feels pretty natural.
JS tc39 proposal for ?.. Kotlin and Swift uses them too.

I think the problem for newcomers are the bind operators ?.+ and ?#+.

@jaredly
Copy link
Contributor

jaredly commented Aug 14, 2018

But the idea of promoting attribute access at this syntax level is a very OO kind of a thing (which typescript, js, kotlin, and swift are), and all of those implementations require some amount of type-loose-shanigans.
I think it's notable that Rust (probably Reason's closest cousin) doesn't have a null-access operator.

@Gregoirevda
Copy link
Contributor Author

Gregoirevda commented Aug 14, 2018

Note that you can do

let name = switch response {
 | None => None
 | Some({user: {name}}) => Some(name)
};

when it's a record, but not when it's a JS object. That's the original issue.

@jaredly
Copy link
Contributor

jaredly commented Aug 14, 2018

So with a javascript object we could similarly do

open Option; // assuming it gives us map, bind
let userName = response->bind(_##user)->bind(_##name)
// or, for if you want more clarity/verbosity
let userName = response->bind(response => response##user)->bind(user => user##name)

I think this gets what we need (freedom from endless switch statements) without adding complexity to the language

@alexeygolev
Copy link

@jaredly this is very similar to Scala's placeholder syntax which I miss very much

@cloudkite
Copy link

cloudkite commented Aug 15, 2018

@cloudkite
Copy link

cloudkite commented Aug 15, 2018

@Gregoirevda feels a little weird to have differentiate between bind and map, can the parser not detect and write correct statement according to the type?

@jaredly
Copy link
Contributor

jaredly commented Aug 15, 2018

@cloudkite Rust has an operator for propagating errors - a ? suffix that will early return with an error from the function.

can the parser not detect and write correct statement according to the type?

definitely not, unfortunately -- the parser has no knowledge of types -- the type inference phase runs well after refmt is done doing its work.

@Gregoirevda
Copy link
Contributor Author

@jaredly coming from Standard ML, OCaml has, among others, OO paradigm: Object, Classes and inheritence. Rust, even if coming from Standard ML, isn't OO. So, saying that ?. is only for OO languages like JS, isn't totaly correct I think.

About Kotlin and Swift, let s hope reason-react-native will be a success. Android and IOS devs will probably appreciate the ?. they're used too.

Even better than cmd+clickable, with 3 lines of code comment to explain what the function is doing, is good naming for functions.
?. seems like a perfect 'naming'.

@cloudkite unfortunately, the parsers just gets strings and tokens.

@cristianoc
Copy link
Contributor

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

@jaredly
Copy link
Contributor

jaredly commented Aug 30, 2018

😮🤯👏👏👏 that's veryyy cool

@leostera
Copy link

Feels a bit misleading to auto-unwrap 🤔 what's the difficulty in learning about >>=, >>|, or <|> ?

let username = response >>= owner >>| name <|> "no_username_available";
"Hello " ++ username

Defining custom operators like above is already completely supported, has plenty of literature around it, has no significant impact on the syntax of the language, and can be even optimized away by the compiler (like Flambda does).

This is starting to smell a lot like the -> debate 😛

@Gregoirevda
Copy link
Contributor Author

Ideally, we should have record typing for Javascript objects.
Record typing already solved this, it's all about bsb here. So adding an infix operator to solve a bsb problem is not the solution.

I'll leave this open until some proposition is made in bsb

@jaredly
Copy link
Contributor

jaredly commented Oct 31, 2018

I made a ppx to try out this safe-call kind of thing (to relieve the pain of working with graphql results) in userland https://github.com/jaredly/get_in

@Gregoirevda
Copy link
Contributor Author

Cool you made a ppx for it!

@jaredly
Copy link
Contributor

jaredly commented Oct 31, 2018

The idea being that we can prove out the concept for a while, and then have a better idea of how much it is needed, etc. to decide whether to add it to the core syntax :)

@shinzui
Copy link

shinzui commented Oct 31, 2018

@Gregoirevda

coming from Standard ML, OCaml has, among others, OO paradigm: Object, Classes and inheritence.

Seasoned OCaml developers avoid the OO features of OCaml, some of them even wish they can remove it from the language. It was added mostly as a marketing ploy to compete with Java.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants