Skip to content
This repository has been archived by the owner on Jun 15, 2023. It is now read-only.

Add if let syntax #12

Merged
merged 2 commits into from
Jul 5, 2020
Merged

Add if let syntax #12

merged 2 commits into from
Jul 5, 2020

Conversation

tomgasson
Copy link
Contributor

@tomgasson tomgasson commented Jul 3, 2020

if let brings pattern matching to if

if let PAT = EXPR { BODY }

The motivation for having any construct at all for this is to simplify the cases that today call for
a switch statement with a single non-trivial case. This is predominately used for unwrapping
option<'t> values, but can be used elsewhere.

Before:

switch(optVal) {
    | Some(x) => doSomethingWith(x)
    | _ => ()
}

After:

if let Some(x) = optVal {
    doSomethingWith(x)
}

Complex example:

if let Some(x) = optVal when x > 10 {
    doSomethingWith(x)
} else if let Theme({color: Red | Blue, emoji}) = getTheme() {
    doSomething()
} else {
    doSomethingElse()
}

There are >500 occurrences in FB of | _ => () where this form would be more appropriate

This is syntax sugar only, it's implemented similar to ns.ternary so that that it should be stable across parsing and printing

Prior Art

reasonml/reason#1301

@IwanKaramazow
Copy link
Contributor

Good stuff! Looks good at first sight, I'll do a more thorough review during the weekend.

@bloodyowl
Copy link
Contributor

I like the idea, but feel like the legibility is pretty hard.

I'd find it clearer to read something like the following:

if optVal match Some(x) {
  doSomethingWith(x)
}

What do you think?

@tomgasson
Copy link
Contributor Author

@bloodyowl
Rust also discussed optVal is Some(x) in a similar form to your optVal match Some(x) suggestion, and I think the same reasoning would apply here rust-lang/rfcs#160 (comment)

  • let is already a keyword an this is just a new position to use it
  • let disambiguates the parsing immediately after if
  • The ordering is already equivalent with let bindings let MyVal(x) = foo()

@tomgasson
Copy link
Contributor Author

Good stuff! Looks good at first sight, I'll do a more thorough review during the weekend.

Thanks - I haven't paid too much attention to breadcrumbs/regions, exception cases or doc printing so could probably use some guidance on how to do them best

@bloodyowl
Copy link
Contributor

@tomgasson Good points yeah. I guess that's where the most sensible tradeoffs are made.

@chenglou
Copy link
Member

chenglou commented Jul 4, 2020

So to be clear, no when support nor let chaining for now right? These can be postponed

I'm still torn between if let Some(a) = result and if let a = result. I'm not sure the generality of the former is worth it. I'm also not sure the latter covers enough.

@cristianoc
Copy link
Contributor

cristianoc commented Jul 4, 2020

Wondering how nested optional record fields look like.

Before:

// type point = { x:int, y:int, z:option<int> }
// type nested = { origin:option<point>, name:string }

let getZ = nested =>
  switch nested.origin {
  | None => 0
  | Some(point) =>
    switch point.z {
    | None => 0
    | Some(z) => z
    }
  }

after:

let getZ = nested =>
  if let Some(point) = nested.origin  {
    if let Some(z) = point.z  {
      z
    } else {
      0
    }
  } else {
    0
  }

@tomgasson notices there's an extra space before{ on the second line.

Copy link
Contributor

@IwanKaramazow IwanKaramazow left a comment

Choose a reason for hiding this comment

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

Great work. Left a couple of questions w.r.t style/naming, the gist of this PR is actually very good.
I share the same philosophy as Cheng, let's maybe drop the support for when in V1? We can always add it back later.

Can you maybe add some extra test cases with nested if lets (simple and complex) and ternaries a ? b : c intermixed?
Cristiano's example is a good start.

| _ ->
parseIfCore startPos p

and parseIfExpression p =
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we rename this into parseIfOrIfLetExpr? Makes it clearer what we're possibly parsing here.

Parser.leaveBreadcrumb p Grammar.ExprIf;
let startPos = p.Parser.startPos in
Parser.expect If p;
let ifBody = parseIfBody startPos p in
Copy link
Contributor

Choose a reason for hiding this comment

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

I would bring parseIfBody inline here. The general idea is to have something like parseAOrB indicating that there are two possible things: A and B. parseAOrB checks the next token and then delegates to the correct function. Example:

and parseIfOrIfLetExpr p =
  ...
  let expr = match p.token with
    | Let -> parseIfLetExpr …
    | _ -> parseIfExpr …
  in
  ...

Ast_helper.Exp.case (Ast_helper.Pat.any ()) elseExpr;
]

and parseIfBody startPos p =
Copy link
Contributor

Choose a reason for hiding this comment

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

See comments, we can bring this inline.

Ast_helper.Exp.ifthenelse ~loc conditionExpr thenExpr elseExpr

and parseIfLet startPos p =
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency I would rename this to parseIfLetExpr to indicate that we're going to parse something of the expression flavour.

Parser.leaveBreadcrumb p Grammar.IfCondition;
(* doesn't make sense to try es6 arrow here? *)
let conditionExpr = parseExpr ~context:WhenExpr p in
Parser.eatBreadcrumb p;
conditionExpr

and parseIfBranch p =
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we rename this to parseThenBranch for clarity?

Parser.expect Rbrace p;
blockExpr;

and parseIfCore startPos p =
Copy link
Contributor

Choose a reason for hiding this comment

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

"Core" is a bit opaque, would it be clearer if we named this just parseIfExpr?

| IfLet (pattern, conditionExpr, guard) ->
let patternDoc = printPattern pattern cmtTbl in
let conditionDoc = printExpressionWithComments conditionExpr cmtTbl in
let guardDocs = match guard with
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be removed as we're not supporting guards in v1?

@yawaramin
Copy link

Does this PR suppress the fragile pattern match warning (4)? I guess it wouldn't, correct?

@tomgasson
Copy link
Contributor Author

@yawaramin: If there's an else { expr } it's encoded as | _ => expr and if not there's an implicit | _ => () used, so this will never trigger that warning

@yawaramin
Copy link

yawaramin commented Jul 4, 2020

@tomgasson thanks. What I'm thinking of is something like:

let x = if let Ok(_) = result { 1 } else { 0 }

...desugaring to:

let x = switch result {
  | Ok(_) => 1
  | _ => 0
}

...which indeed seems to trigger the warning.

@tomgasson
Copy link
Contributor Author

tomgasson commented Jul 4, 2020

@yawaramin: Ah, I see. Yeah I think we should then suppress it then, since the intention here is to allow that

but you've raised a few more cases of interest to me

let x = if let Ok(_) = result {
  1
} else if let Error(_) = result {
  2
} else {
  0
}

...the else will never be taken. else if becomes a nested switch rather than lifting the conditions up to the same level because that's better in the general case (if it was getResult() and side-effecting or if they were different result and other)

let x = if let Ok(_) | Error(_) = result {
  1
} else {
  0
}

...the else will never be take. The existing warning should let us know

@yawaramin
Copy link

yawaramin commented Jul 4, 2020

@tomgasson yeah, in that case we would hit warning 11 (this match case is unused). But just to clarify, you do want to suppress warning 4 for this construct.

Another question, will warning 11 wording also be adjusted to reflect the sugar syntax? I.e. it would be confusing to see this match case is unused for an unused else clause. Although to be somewhat fair, the wording is used for Reason's current switch anyway.

@tomgasson
Copy link
Contributor Author

Updated to suppress warning 4.

For the wording of 11, I think we need the capability to rewrite warnings as mentioned in #1

@IwanKaramazow
Copy link
Contributor

@tomgasson Merging, thanks for the implementation. Nice work!

@IwanKaramazow IwanKaramazow merged commit d10ac6e into rescript-lang:master Jul 5, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants