Proposing new syntax for zero-cost unwrapping options/results

To be fair, if reconnectAndGetConn returns a compatible error type, this would presumably allow us to try to recover, right?

let? Ok(r) = switch getConn() {
| Ok(x) => Ok(x)
| Error(ErrorA) =>
  reconnectAndGetConn() // hypothetical recover
| Error(ErrorB) =>
  Error(ErrorB) // not possible?
}
2 Likes

Good point, that’d definitely work.

I’m all for this proposal

At the moment switch case verbosity is so bad my Rust implementation has the same amount of lines as Rescript.

let? Makes a lot of sense, the ? Shows the None is quick returned, and the Some shows what you are looking for.

And competes with Gleam’s use var <- try(fn())

Dart and Rust also have eager error handling with ?

GoLang’s 3lines of if err != nil after every functionality makes me nauseous

Playing devils advocate: would this work? let? Error(err) = fn() as an early happy return.

1 Like

We could definitely support let? Error(err) = ... and let None = ... if that adds any value. Just a matter of swapping what’s emitted for the continue vs return case.

1 Like

In let? Ok(user) = fetchUser()

If you see let? as testing if fetchUser() returns an Ok then yes I’d expect the reverse to be possible too, this also aligns to what we are used to in switch pattern matching.

4 Likes

@lil5 I updated the PR supporting let? Error(e) = ... and let? None = .... PoC of let? by zth · Pull Request #7582 · rescript-lang/rescript · GitHub

5 Likes

Looks good to me! Ship it! :wink:

4 Likes

Happy to say this was just merged under the new concept of “experimental features” (that you will be able to turn on/off in rescript.json) and will be in the next beta that’ll be released this week :tada:

11 Likes

This would be nice addition to ReScript! :heart:

Probably I’m late to the party, but here’s the shower thought. The proposed syntax is a little bit inconsistent with promises if we look at promises, options, and results as some kind of boxes/wrappers/monadish-things that either resolve to a “good” or “bad” value. Both let? and await are basically shortcuts to express:

  • Unbox the value and if it’s good, assign and go on;
  • otherwise return the bad value putting it in the box again

In the case of promises the syntax looks like this:

let variable = <unboxing_designator> <expression>

whereas in the case of options and results it has the designator in another position:

let<unboxing_designator> variable = <expression>

With further comparison, the await is more versatile in the sense it evaluates to expression, not a statement, so the following is possible:

let something = foo(await bar(), await baz())

With everything mentioned in this thread I understand that the current implementation requires that the designator should be somewhere very close to let, but I wonder if such asymmetry is fine in the long run.

Just thinking aloud: what if unwrapping be more like await?

let bar: unit => result<string, [#errA | #errB]>
let baz: unit => result<string, [#errA | #errC]>
let foo: (string, string) => string

let myFunc = (): result<string, [#errA | #errB | #errC]> => {
  let something = foo(open bar(), open baz())  // <- reusing `open` keyword, might be `try` if not followed by `{`
  something
}
4 Likes

I played with this last week.

Some feedback:

  • This should also work for Null.t and Nullable.t.
  • It would be more ergonomic if we could mix various types. For example:
let updateH2Color2 = () => {
  let? Some(element) = dollar("h2")
  let? Ok(h2) = element->tryMapHTMLHeadingElement
  h2.style.color = "blue"
  Some()
}

could maybe become

let updateH2Color2 = () => {
  @let.unwrap
  switch dollar("h2") {
  | None => Error(xyz) // not sure yet how to define xyz
  | Some(element) =>
    @let.unwrap
    switch tryMapHTMLHeadingElement(element) {
    | Error(_)  as x => x
    | Ok(h2) =>
      h2.style.color = "blue"
      Some()
    }
  }
}

I get that xyz needs to come from somewhere, so that is challenging, I agree. But mixing option, null and nullable should be more straightforward. You could return the empty case of the last let?, I think.

In my examples, I was also grabbing data to finally mutate something. The last meaningful expression was h2.style.color = "blue". Having to return result<unit, _> didn’t add that much value. Again, this depends on the situation. This could have been:

let updateH2Color2 = () => {
  @let.unwrap
  switch dollar("h2") {
  | None => ()
  | Some(element) =>
    @let.unwrap
    switch tryMapHTMLHeadingElement(element) {
    | Error(_)  => ()
    | Ok(h2) =>
      h2.style.color = "blue"
    }
  }
}

Of course, I don’t know how easy these suggestions are to implement.

1 Like

I like your ideas. I’m already using v12 with let-unwrap in production, loving it

1 Like

You can btw. now test this feature in the ReScript playground as well:

Toggle on/off in settings under the new “Experimental Features” section.

4 Likes

i’m already using this feature in production, absolutely loving it, best thing introduced in rescript since async-await

2 Likes

Do you have any examples of how you’re typically using it? Before/after code? Might serve as great inspiration.

3 Likes

Or as a more practical example for the blog post I want to write about it :slight_smile:

2 Likes

sure, here’s some from one of my rescript code bases:

...

/**
Parses three parsers in sequence, returning a triple of their results.
All must succeed.
*/
let seq3 = (p1, p2, p3): parser<_> => {
  st => {
    let? Ok((v1, st1)) = p1(st)
    let? Ok((v2, st2)) = p2(st1)
    let? Ok((v3, st3)) = p3(st2)
    Ok(((v1, v2, v3), st3))
  }
}

/**
Parses four parsers in sequence, returning a quadruple of their results.
All must succeed.
*/
let seq4 = (p1, p2, p3, p4): parser<_> => {
  st => {
    let? Ok((v1, st1)) = p1(st)
    let? Ok((v2, st2)) = p2(st1)
    let? Ok((v3, st3)) = p3(st2)
    let? Ok((v4, st4)) = p4(st3)
    Ok(((v1, v2, v3, v4), st4))
  }
}

...

it’s from our small in-house rolled parser combinator lib

from the same project in a different place:

...

  // ── Lexing ──
  let? Ok(toks) = switch lexer->DSL_Compilation_Lexer.tokenize(src) {
  | Ok(r) => Ok(r)
  | Error({span, msg}) => Error(CompilationError.UnknownToken({span, msg}))
  }

  // ── Parsing ──
  let? Ok(ast) = switch parser.parseAll(toks) {
  | Ok(r) => Ok(r)
  | Error({kind: UnexpectedToken, span, msg}) =>
    Error(CompilationError.UnexpectedToken({span, msg}))
  }


...
3 Likes

Amazing; can’t wait to see this ship!

3 Likes

sorry i missed this. i don’t have it, but you can imagine nesting 5 switches for seq4 :wink: im not sure we want to put that into text ever again!

Here is a great example:

this becomes:

let object25 = (
  destruct,
  construct,
  field1,
  field2,
  field3,
  field4,
  field5,
  field6,
  field7,
  field8,
  field9,
  field10,
  field11,
  field12,
  field13,
  field14,
  field15,
  field16,
  field17,
  field18,
  field19,
  field20,
  field21,
  field22,
  field23,
  field24,
  field25,
) =>
  Codec.make(
    // encode
    value => {
      let (
        val1,
        val2,
        val3,
        val4,
        val5,
        val6,
        val7,
        val8,
        val9,
        val10,
        val11,
        val12,
        val13,
        val14,
        val15,
        val16,
        val17,
        val18,
        val19,
        val20,
        val21,
        val22,
        val23,
        val24,
        val25,
      ) = destruct(value)
      jsonObject([
        Field.encode(field1, val1),
        Field.encode(field2, val2),
        Field.encode(field3, val3),
        Field.encode(field4, val4),
        Field.encode(field5, val5),
        Field.encode(field6, val6),
        Field.encode(field7, val7),
        Field.encode(field8, val8),
        Field.encode(field9, val9),
        Field.encode(field10, val10),
        Field.encode(field11, val11),
        Field.encode(field12, val12),
        Field.encode(field13, val13),
        Field.encode(field14, val14),
        Field.encode(field15, val15),
        Field.encode(field16, val16),
        Field.encode(field17, val17),
        Field.encode(field18, val18),
        Field.encode(field19, val19),
        Field.encode(field20, val20),
        Field.encode(field21, val21),
        Field.encode(field22, val22),
        Field.encode(field23, val23),
        Field.encode(field24, val24),
        Field.encode(field25, val25),
      ])
    },
    // decode
    json =>
      json
      ->asObject
      ->Result.flatMap(fieldset => {
        let? Ok(val1) = field1->Field.decode(fieldset)
        let? Ok(val2) = field1->Field.decode(fieldset)
        let? Ok(val3) = field1->Field.decode(fieldset)
        let? Ok(val4) = field1->Field.decode(fieldset)
        let? Ok(val5) = field1->Field.decode(fieldset)
        let? Ok(val6) = field1->Field.decode(fieldset)
        let? Ok(val7) = field1->Field.decode(fieldset)
        let? Ok(val8) = field1->Field.decode(fieldset)
        let? Ok(val9) = field1->Field.decode(fieldset)
        let? Ok(val10) = field1->Field.decode(fieldset)
        let? Ok(val11) = field1->Field.decode(fieldset)
        let? Ok(val12) = field1->Field.decode(fieldset)
        let? Ok(val13) = field1->Field.decode(fieldset)
        let? Ok(val14) = field1->Field.decode(fieldset)
        let? Ok(val15) = field1->Field.decode(fieldset)
        let? Ok(val16) = field1->Field.decode(fieldset)
        let? Ok(val17) = field1->Field.decode(fieldset)
        let? Ok(val18) = field1->Field.decode(fieldset)
        let? Ok(val19) = field1->Field.decode(fieldset)
        let? Ok(val20) = field1->Field.decode(fieldset)
        let? Ok(val21) = field1->Field.decode(fieldset)
        let? Ok(val22) = field1->Field.decode(fieldset)
        let? Ok(val23) = field1->Field.decode(fieldset)
        let? Ok(val24) = field1->Field.decode(fieldset)
        let? Ok(val25) = field1->Field.decode(fieldset)

        construct((
          val1,
          val2,
          val3,
          val4,
          val5,
          val6,
          val7,
          val8,
          val9,
          val10,
          val11,
          val12,
          val13,
          val14,
          val15,
          val16,
          val17,
          val18,
          val19,
          val20,
          val21,
          val22,
          val23,
          val24,
          val25,
        ))
      }),
  )
1 Like

Maybe not the most common thing in a code base, but I guess it illustrates the point very well.

2 Likes