Solution in moc 0.8.3: let else - "match" and "take" in motoko - do? for variants (was "when")

Could we get something like this in motoko?

let z : ?Text = when(x, #text(val)){val.value});

It would basically return x if the pattern matches and null if it doesn’t? Or maybe there is another way to do this. I was experimenting on the playground below and almost got there but a trap keeps me from failing well and try/catch doesn’t seem to catch it.

https://m7sm4-2iaaa-aaaab-qabra-cai.raw.ic0.app/?tag=2252837445

Is there a way to capture this error and continue?

Method: say (query)
“Status”: “rejected”
“Code”: “CanisterError”
“Message”: “IC0503: Canister 475h5-dyaaa-aaaab-qac4a-cai trapped explicitly: pattern failed”

I’ve tried a try/catch, but that doesn’t seem to be working and I suspect I can only use that in an async function. How do you ignore recover from this in motoko?

I also tried:
I also tried

private func test((#text(val) or #nat(val2): testtype)) : Text {
      return val.value;
  };

But it didn’t like variables with additive patterns or something like that.

1 Like
    let z = switch(x) {
        case (#text(val)) {?val.value};
        case _ {null}
    };

Yeah. Switch statements. But when you use these in production they get insane. I had direct criticism yesterday from a code auditor that the code was extremely hard to read due to the nested switch statements. They are very hard to manage when you want to get information back to the user and not just throw/assert.

1 Like

I would suggest writing switch statements once and then using them elsewhere.

Something like this, or variations thereof:

module Foo {
  public type Foo = {
    #nat : Nat;
    #text : Text;
  };

  public func nat(foo : Foo) : ?Nat {
    switch(foo) {
        case (#nat(value)) ?value;
        case _ null;
    };
  };
  
  public func text(foo : Foo) : ?Text {
    switch(foo) {
        case (#text(value)) ?value;
        case _ null;
    };
  };
};
let y : ?Nat = Foo.nat(#nat(42)); // ?42
let z : ?Text = Foo.text(#text("Hello")); // ?"Hello"

Not sure how useful it is for the use case you have in mind since you can’t construct things like this anyway:

EDIT: Silly mistake, this works fine and should address your concern:

let x : ?Text = Foo.nat(#text("Hello"));  // null

Paul’s solution above is pretty good (I would probably use that), but to add on there are some pretty nifty helper functions in motoko-base/Option.mo at master · dfinity/motoko-base · GitHub if you’re looking just to see that a value exists and not necessarily to unwrap that value. Depending on your use case and the number of types you’re trying to narrow down, you may not even need a variant/switch statement in the first place.

Specifically, Option.get() favors a pattern that produces the value or a default, and Option.isSome() is essentially the exists() pattern.

1 Like

Yep…I’ve started to use some of those in certain situations and they are great help. The Option.get is frustrating because if you any kind of complex type you have to create a “default” for it. Not a big deal, but if the type is complex things like equivalence become difficult.

I would love something like Option.get(x, return #error(“some error”), but you can’t do that because the second parameter is executed before being passed to the function.

Using @paulyoung 's I could do

let x = if(Foo.nat(y) != null){return Foo.nat(y);}ele {return #error("some error"};

but that is almost as hard as having to read as scrolling up to figure out what .nat does. I’m interested in trying to increase the amount of information conveyed to the reader with the fewest bits possible and about 80% of the solution is boilerplate.

If I’ve tested if something is null and reacted to it I probably want to use the unwrapped version elsewhere in my code. So I end up with this pattern everywhere:

let x :Nat = switch(x){
     case(null){ return #error("x was null, try again")};
     case(?x){x};
}

I’ve highlighted the cruft that conveys no information:

Something like the following would make the language much more elegant

              //unwrap the variable unless it nulls out; if it nulls out then execute the block
let x : Text = take(x!){return #error("x was null, try again)};

and

               //match the var to the pattern and execute the block; else return null
let x : ?Text = match(x,#variant(val)){val;}

together

let x : Text = take(match(x, #variant(val){val}){ return #error("x was null, try again)};
3 Likes

I’m a bit confused about the types involved in your example. What is the type of x? I imagine the problem isn’t to do with evaluation but rather with type checking.

Are you trying to early return from some function scope here? Otherwise it looks like you’re trying to unify Text and { #error : Text }

I started a general purpose package based on well established principles to do exactly this a while back but wasn’t satisfied with the outcome.

Maybe it’s time for another look with fresh eyes.

3 Likes

I’m a bit confused about the types involved in your example. What is the type of x ? I imagine the problem isn’t to do with evaluation but rather with type checking.

For this one I want an early return to the user telling them that something isn’t found or configured

let found_user : User = Option.get(sate.users.get(user_id), return #err(“user not found”)); //dont do this or you will be up until three am convinced you are going crazy and your significant other will be mad at you in the morning when you fall asleep at the breakfast table

Are you trying to early return from some function scope here? Otherwise it looks like you’re trying to unify Text and { #error : Text }

Exactly! I want to return here and tell the user to get their stuff together before calling the is function again. You wouldn’t have to return #err there, you could provide a default…so it would be like Option.get but with an in context executable block that only gets executed if the bang hits.

I suspect Result Is the way to do this, but there isn’t built in syntax for that. If you have a more complete example of what you want to do with found_user I could demonstrate how to do that.

There are ways to do that lazily but I still think the types are the problem.

You could use do ? for this. Assume x : ?Nat

let res : ?Nat = do ? {
  let y = x!;
  let z = Foo.nat(foo_variant)!;
  y + z
}

If res is null, it means one of the ! is null, but don’t know which one.

1 Like

Right…but my frustration with this is that if I’m solving for x it is likely because I want to add it something or subtract it or do something. When I get there in my code I have to do another switch to get it out of the ?Nat.

@skilesare, or use another do. That is natural and intentional: hitting null is a kind of failure, and if there is the possibility of failure somewhere earlier in a code path, it has to propagate outwards. The type system does not allow you to “forget” that possibility. Null has to either be handled or propagated; do is the convenient way to propagate it, switch a way to handle it.

import Buffer "mo:base/Buffer";
import RB "mo:base/RBTree";
import Nat "mo:base/Nat";
import Iter "mo:base/Iter";
import Time "mo:base/Time";
import Result "mo:base/Result";

actor Echo {

  type Gender = {
          #M;
          #F;
          #O: Text;
      };

  type User = {
      id : Nat;
      var fname: Text;
      var mi: ?Text;
      var lname: Text;
      var email: ?Text;
      var access_count: Nat;
      var gender: Gender;
      access_log: Buffer.Buffer<(Nat, Int)>;
  };  

  let users = RB.RBTree<Nat, User>(Nat.compare);
  var nonce = 1;

  let aUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat, Int)>(1);
  };

  let bUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat, Int)>(1);
  };

  let cUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat,Int)>(1);
  };

    

  //50 lines
  public shared(msg) func query_gender(user_id : Nat, access_id : Nat) : async Result.Result<Text, Text> {
    let foundAccessUser = switch(users.get(access_id)){
        case(null){return #err("access user not found")};
        case(?val){val};
    };

    //let foundAccessUser = take(user.get(access_id)){return "access user not found"}

    let foundUser = switch(users.get(user_id)){
        case(null){return #err("user not found")};
        case(?val){val};
    };

    foundUser.access_log.add((foundAccessUser.id, Time.now()));

    
    return #ok(switch(foundUser.gender){
        case(#M){"male"};
        case(#F){"female"};
        case(#O(val)){val};
    });
  };

  // Say the given phase.
  public shared(msg) func is_female(user_id : Nat, access_id : Nat) : async Result.Result<Bool, Text> {
    let foundAccessUser = switch(users.get(access_id)){
        case(null){return #err("access user not found")};
        case(?val){val};
    };

    //let foundAccessUser = take(user.get(access_id)){return "access user not found"}

    let foundUser = switch(users.get(user_id)){
        case(null){return #err("user not found")};
        case(?val){val};
    };

    foundUser.access_log.add((foundAccessUser.id, Time.now()));

    switch(foundUser.gender){
        case(#O(val)){return #err("nyi for custom gender")};
        case(_){}
    };

    return #ok(switch(foundUser.gender){
        case(#M){false};
        case(#F){true};
        case(#O(val)){true//unreachable
        };
    });
  };

/* 27 lines
  public shared(msg) func query_gender(user_id : Nat, access_id : Nat) : async Result.Result<Text, Text> {
    let foundAccessUser = take(users.get(access_id)!){return #err("access user not found")};};
    let foundUser = take(users.get(user_id)!{return #err("user not found")};

    foundUser.access_log.add((foundAccessUser.id, Time.now()));

    return #ok(switch(foundUser.gender){
        case(#M){"male"};
        case(#F){"female"};
        case(#O(val)){val};
    });
  };

  public shared(msg) func is_female(user_id : Nat, access_id : Nat) : async Result.Result<Bool, Text> {
    let foundAccessUser = take(users.get(access_id)!){return #err("access user not found")};};
    let foundUser = take(users.get(user_id)!{return #err("user not found")};

    foundUser.access_log.add((foundAccessUser.id, Time.now()));

    match(foundUser.gender, #O(val)){return #err("nyi for custom gender")};

    return #ok(switch(foundUser.gender){
        case(#M){false};
        case(#F){true};
        case(#O(val)){true//unreachable
        };
    });
  };
*/
  


}; 

Will respond soon with some suggestions.

I would start by doing something like this:

import Buffer "mo:base/Buffer";
import RB "mo:base/RBTree";
import Nat "mo:base/Nat";
import Iter "mo:base/Iter";
import Time "mo:base/Time";
import Result "mo:base/Result";

actor Echo {

  type Gender = {
      #M;
      #F;
      #O: Text;
  };

  type User = {
      id : Nat;
      var fname: Text;
      var mi: ?Text;
      var lname: Text;
      var email: ?Text;
      var access_count: Nat;
      var gender: Gender;
      access_log: Buffer.Buffer<(Nat, Int)>;
  };

  let users = RB.RBTree<Nat, User>(Nat.compare);
  var nonce = 1;

  let aUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat, Int)>(1);
  };

  let bUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat, Int)>(1);
  };

  let cUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat,Int)>(1);
  };

  func isFemale(gender : Gender) : Bool {
    switch(gender) {
      case(#M) false;
      case(#F) true;
      case(#O(_)) false;
    };
  };

  func isOther(gender : Gender) : Bool {
    switch(gender) {
      case(#M) false;
      case(#F) false;
      case(#O(_)) true;
    };
  };

  func printGender(gender : Gender) : Text {
    switch(gender) {
      case(#M) "male";
      case(#F) "female";
      case(#O(val)) val;
    };
  };

  public shared(msg) func query_gender(user_id : Nat, access_id : Nat) : async Result.Result<Text, Text> {
    let accessUser = Result.fromOption(users.get(access_id), "access user not found");

    Result.chain<User, Text, Text>(accessUser, func (foundAccessUser) {
      let user = Result.fromOption(users.get(user_id), "user not found");

      Result.chain<User, Text, Text>(user, func (foundUser) {
        foundUser.access_log.add(foundAccessUser.id, Time.now());
        #ok(printGender(foundUser.gender));
      });
    });
  };

  public shared(msg) func is_female(user_id : Nat, access_id : Nat) : async Result.Result<Bool, Text> {
    let accessUser = Result.fromOption(users.get(access_id), "access user not found");

    Result.chain<User, Bool, Text>(accessUser, func (foundAccessUser) {
      let user = Result.fromOption(users.get(user_id), "user not found");

      Result.chain<User, Bool, Text>(user, func (foundUser) {
        foundUser.access_log.add(foundAccessUser.id, Time.now());

        if (isOther(foundUser.gender)) {
          #err("nyi for custom gender");
        } else {
          #ok(isFemale(foundUser.gender));
        }
      });
    });
  };
};

I would then extract the common parts to separate functions, like this:

import Buffer "mo:base/Buffer";
import RB "mo:base/RBTree";
import Nat "mo:base/Nat";
import Iter "mo:base/Iter";
import Time "mo:base/Time";
import Result "mo:base/Result";

actor Echo {

  type Gender = {
      #M;
      #F;
      #O: Text;
  };

  type User = {
      id : Nat;
      var fname: Text;
      var mi: ?Text;
      var lname: Text;
      var email: ?Text;
      var access_count: Nat;
      var gender: Gender;
      access_log: Buffer.Buffer<(Nat, Int)>;
  };

  let users = RB.RBTree<Nat, User>(Nat.compare);
  var nonce = 1;

  let aUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat, Int)>(1);
  };

  let bUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat, Int)>(1);
  };

  let cUser = {
      id = nonce += 1;
      var fname = "jane";
      var mi = ?"d";
      var lname = "doe";
      var email = ?"z@s.com";
      var access_count = 0;
      var gender = #F;
      access_log = Buffer.Buffer<(Nat,Int)>(1);
  };

  func isFemale(gender : Gender) : Bool {
    switch(gender) {
      case(#M) false;
      case(#F) true;
      case(#O(_)) false;
    };
  };

  func isOther(gender : Gender) : Bool {
    switch(gender) {
      case(#M) false;
      case(#F) false;
      case(#O(_)) true;
    };
  };

  func printGender(gender : Gender) : Text {
    switch(gender) {
      case(#M) "male";
      case(#F) "female";
      case(#O(val)) val;
    };
  };

  func getUser(userId : Nat) : Result.Result<User, Text> {
    Result.fromOption(users.get(userId), "user not found");
  };

  func getAccessUser(accessId : Nat) : Result.Result<User, Text> {
    Result.fromOption(users.get(accessId), "access user not found");
  };

  func access<Ok>(userId : Nat, accessId : Nat, action : User -> Result.Result<Ok, Text>) : Result.Result<Ok, Text> {
    let accessUser = getAccessUser(accessId);

    Result.chain<User, Ok, Text>(accessUser, func (foundAccessUser) {
      let user = getUser(userId);

      Result.chain<User, Ok, Text>(user, func (foundUser) {
        foundUser.access_log.add(foundAccessUser.id, Time.now());
        action(foundUser);
      });
    });
  };

  public shared(msg) func query_gender(user_id : Nat, access_id : Nat) : async Result.Result<Text, Text> {
    access<Text>(user_id, access_id, func (user) {
      #ok(printGender(user.gender));
    });
  };

  public shared(msg) func is_female(user_id : Nat, access_id : Nat) : async Result.Result<Bool, Text> {
    access<Bool>(user_id, access_id, func (user) {
      if (isOther(user.gender)) {
        #err("nyi for custom gender");
      } else {
        #ok(isFemale(user.gender));
      }
    });
  };
};
1 Like

Ahh…interesting, so since you have the wrapped type(in a result) you use the wrapped time functions to unwrap it. These are great suggestions and I’ll incorporate them, but…well it is still …and maybe even more…hard to read and follow what is going on.

One question. access takes a type param so you can use this for a generic. In the second line of access you call chain with Ok(the generic type), but Result.Chain uses <R1, R2, Error> and It thought R1 and R2 had to be result types themselves. What is going on here that marshals your Bool into a R2?