New Let Binding and Serialization Improvements: Motoko Updates

Motoko Biweekly Update Part 8!

Hi Motoko Devs!

Welcome to the latest Motoko update post.

If you missed the last update post from the Motoko team, check it out here! Last time we talked about a generational GC demo and performance optimizations for our VSCode Extension.

Today we’ll be talking about a new let-else binding and improved deserialization in Candid!

New Language Feature: let-else binding!

There is a new let binding known as the let-else binding! let-else works similarly to the usual let binding, except there is an else block that gets executed in case of a pattern match failure.

E.g. assuming foo() returns type ?Nat

let ?x = foo() else { Debug.print "foo failed"; return };

If foo() returns null, null doesn’t match the pattern ?x, and so “foo failed” is printed and execution returns out of the current function. Otherwise, the Nat is bound to x and execution continues forward.

This can be really useful to avoid deeply nested switch statements, such as the following:

switch (foo()) {
  case (#ok x) {
    switch (bar()) {
      case (#ok y) {
        switch (baz()) {
          case (#ok z) {
            return #ok(x + y + z);
          }
          case _ {
            return #err "baz failed";
          }
        } 
      };
      case _ {
        return #err "bar failed";
      }
    }
  };
  case _ {
    return #err "foo failed with";
  }
}

With let-else bindings, you get the following:

let #ok(x) = foo() else { return #err "foo failed" };
let #ok(y) = bar() else { return #err "bar failed" };
let #ok(z) = baz() else { return #err "baz failed" };

return #ok (x + y + z)

This feature was inspired by the same feature in rust.

See the PR and more details here. You can find this feature in version 0.8.3 of moc.

Please let us know your experience working with this binding!

Improved Candid Serialization and Deserialization

Before, it was possible that you could serialize a Motoko value to Candid, but deserializing that encoded value back to Motoko would fail with a trap. It turns out that this was due to the stack consumption of Candid decoding being greater than the stack consumption of Candid encoding, especially when decoding deeply nested, recursive values such as lists. Although Candid related, the problem could also prevent upgrades, since the current stable variable format is based on Candid. Luckily, we managed to diagnose and fix the issue (a premature stack overflow) by putting the Candid decoder on a stack diet. While not a cure-all, our fix should prevent other surprising failures in the future.

See details for this here.

Till next time!

– DFINITY Languages team

26 Likes

Keep up the good work guys. The let-else is super helpful, reduces my switch chaos code

5 Likes

I would argue that this is a somewhat silly example. Normally, the simplest way to write something like this using neither if-else nor switch but do?:

ignore do? { return #ok(foo()! + bar()! + baz()!) };
return #err "something failed";

Unless you really care about different error messages. For that, we’d need a version of do that works for Result types.

3 Likes

I think do ? {} expressions can lead to worse code readability, sometimes exclamation marks are hard to track
I tend to overuse do ? {} expressions in my code and I really like the addition of let-else

3 Likes

Point taken. I’ve updated the example

1 Like

Yes! Thank you. This is really helpful. Does it work with variants?

Edit: It does! https://m7sm4-2iaaa-aaaab-qabra-cai.ic0.app/?tag=723997350

// Say the given phase.
  public query func doesItWork(phrase : Text) : async Text {

    let #possibly(x) = #possibly("something") else { return "nope"};

    return x;
  };

  // Say the given phase.
  public query func doesItFail(phrase : Text) : async Text {

    let #maybe(x) = #possibly("something") else { return "yep"};

    return Nat.toText(x);
  };
3 Likes

Congrats, and thank you to everyone involved with this update. I had a couple questions about the deserialization improvements.

  1. Should I interpret this update to mean we no longer have to worry about catching an error when using from_candid()?

  2. If we don’t have to worry about the method throwing an error do we still need to make it an async method? (i’m assuming the async requirement was there so we could raise the error).

Note: I’m aware these could be bad assumptions; that’s why I’m asking :slight_smile:
Thanks!

1 Like

Last time I looked, from_candid gave an option type back.

Right. But I thought there was also risk of it trapping. I could have sworn I read that somewhere

Edit: It looks like it traps if you give it a blob that is not encoded correctly. So I guess that would still be true here. My bad. Internet Computer Content Validation Bootstrap

Yes, thanks this improves readability on the switch stuff :slight_smile:

I guess that while iterating over something and having complex switch-tree structures (not in ex.) its useful in combination with a label to continue/break instead of returning. As i noticed they are also of type None.
like this Mo-Playground very basic example

1 Like

Is there any reason we can’t have this with var? I noticed it didn’t work.

var ?x = something() else return "error";

?x := somethingelse() else return "error;  //I actually don't know if this is valid if you were just setting it without the x.

Destructuring (pattern matching) doesn’t work with var even without the else part. I am not sure about the exact technical limitations.

1 Like

@claudio

Please correct me if I’m wrong, but I believe there is no technical limitation to doing this, but is more a matter of overloading the language with syntactic sugar.

The l.h.s. of a binding is a pattern, while the l.h.s. of an assignment is an expression. Mixing the two creates both a syntactic and a semantic mess.

3 Likes