Are `if-let` constructs going to be supported?

Compare the following snippet:

switch foo() {
| Some(x) => hello(x)
| _ => ()
}

with

if let Some(x) = foo() {
    hello(x)
}

Right now the compiler says:

If-let is currently highly experimental. Use a regular `switch` with pattern matching instead

which makes me think it is a planned feature. Searching GitHub produces the following results:

Is this going to be supported in a future release?

4 Likes

If there’s enough interest, this can probably be revisited to get to a complete design for evaluation.
It probably needs a champion to bring it forward.

3 Likes

There’s a good reason to always be explicit in your handling of None.

1 Like

Is this only useful for functions producing unit? seems like such a special case

No, I think you can add an else and return a value, but then it’s not clear how it’s better than the regular switch

let result = if let Some(x) = foo() {
  x
} else {
  -1
}
2 Likes

I think it makes unwrapping values easier. More complex example from rescript-lang/syntax#12:

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

Looks more familiar to read as a JS developer than:

switch (optVal, getTheme()) {
| (Some(x), _) => doSomethingWith(x)
| (_, Theme({color: Red | Blue, emoji})) => doSomething(emoji)
| (_, _) => doSomethingElse()
}

Rust and Swift also has if-let support.

4 Likes

ah I see. though the similarity to javascript seems pretty pale to me.
You have switches in js as well right, and are still doing pattern matching the same way
and else if let is a lot more noise than (_, ...

Anyway I am really starting to appreciate haskells “avoid success at all costs” and wish for Rescript to stay as simple as reasonably possible. and one way to do a thing is a good number.

6 Likes

As a JS veteran and very new to Rescript I have to agree. That if-structure is noisy and harder to read.

2 Likes

I don’t have an opinion on if let, but I personally really agree with this. Having as few ways as possible to do something is a big benefit in the long run.

6 Likes

For a proposal like this, I’d like to see some advantages in the nested cases.
For example in this triple nested case

switch o.a {
| Some(x) =>
  switch x.b {
  | Some(y) =>
    switch y.c {
    | Some(z) => z == 3
    | None => false
    }
  | None => false
  }
| None => false
}

it would need to be possible to flatten it. E.g. something like this if possible:

if let Some(x) = o.a && Some(y) == x.b && Some(z) == y.c {
  z == 3
} else {
  false
}

And in mixed cases such as the following:

switch o.a {
| Some(x) =>
  switch x.b {
  | Some(y) =>
    switch y.c {
    | Some(z) => z
    | None => 1
    }
  | None => 0
  }
| None => 0
}

if should degrade gracefully as in:

if let Some(x) = o.a && Some(y) == x.b {
  if let Some(z) == y.c {
    z
  } else {
    1
  }
} else {
  0
}

In general, those are the kind of properties I would look for: it should give clear advantages for nested cases, and have no additional runtime cost compared to the explicit switches.

7 Likes

Since you can match multiple variables in one go, you’d never need to nest match-expressions like that.

We have implemented Option.allN For this case so our code reads:

switch Option.all2(xData, yData) {
  | Some((x, y)) => chart2D(x,y)
  | _ => ()
}

(Doesnt always start there but its a small refactor as i go through code)

2 Likes

Notice the examples use nested options not multiple options.
The type is:

{a: option<{b: option<{c: option<...> }>}>}

Not this:

(option<...>, option<...>, option<...>)
1 Like

OK, well, you can still do it in one match-expression, if needed. :slight_smile:

ah ok…Still so much structure to parse in one line…
Option.flatMap into Option.all

Hello, new to Rescript.
This conversation seems related to my issue.

What is the best way to write this?

for lnum in 1 to Array.length(lines) {
  let someline = lines[lnum - 1]
  switch someline {
  | Some(line) => {
      let regexout = firstChar->Js.Re.exec_(line)
      switch regexout {
      | Some(result) =>
        let first = Js.Re.captures(result)[0]
        switch first {
        | Some(char) =>
          switch Js.Nullable.toOption(char) {
          | Some("#") => Js.log("Maybe a title")
          | Some(":") => Js.log("Maybe a substitution")
          | Some("[") => Js.log("Maybe an attribute")
          | _ => Js.log("Something else")
          }
        | None => ()
        }
      | None => ()
      }
    }
  | None => ()
  }
}

Each switch depends on a fresh variable so I don’t think I can switch on multiple variables in that case, can I?

1 Like

This is a use-case for the so called maybe monad, but I’m not sure the syntax is available in ReScript.

You could get rid of the nested switch hell by using utility functions & the -> (pipe) operator:

open Belt

for lnum in 1 to Array.length(lines) {
  let someline = lines[lnum - 1]
  switch someline
  ->Option.flatMap(firstChar->Js.Re.exec_)
  ->Option.flatMap(result => Js.Re.captures(result)[0])
  ->Option.flatMap(Js.Nullable.toOption) {
  | Some("#") => Js.log("branch1")
  | Some(":") => Js.log("branch2")
  | Some("[") => Js.log("branch3")
  | _ => Js.log("something else")
  }
}

Functional programming requires a different mindset to problem solving without letting imperative thinking get in the way. I recommend a lot of practice

5 Likes

Yeah, this is needed when you don’t have monadic notation. :slight_smile: Like the latest version of OCaml actually does have (let* and such). :face_with_hand_over_mouth:

2 Likes

Yeah, like the do-notation in Haskell or computation expressions in F#

1 Like