motoko icon indicating copy to clipboard operation
motoko copied to clipboard

`from_candid()` can't decode tuple types?

Open iclighthouse opened this issue 2 years ago • 13 comments

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


See below a few ways to accomplish what you want!

iclighthouse avatar Jul 18 '23 06:07 iclighthouse

Weird but true.

This works:

    let blob = to_candid("text", 1);
    return from_candid(blob);

This too:

assert ?("text", 1) == (from_candid(to_candid("text", 1)) : ?(Text, Nat));

And we have tests like that.

But this won't work:

assert ?("text", 1) == from_candid(to_candid(("text", 1))) : ?((Text, Nat));

~I believe the parser (for to_candid) doesn't implement the rule~: "Parentheses can be removed (are redundant) as long as the arity of the call doesn't change". ~to_candid is always unary, so~

assert to_candid(("text", 1)) == to_candid("text", 1);

~should hold. But it doesn't!~

Eeek,

public query func encode2() : async (Blob, Blob) {
    let data: (Text, Nat) = ("text", 1);
    return (to_candid(data), to_candid("text", 1));
  };

demonstrates that to_candid("text", 1) is a binary (two-argument) encoding:

( vec {68; 73; 68; 76; 1; 108; 2; 0; 113; 1; 125; 1; 0; 4; 116; 101; 120; 116; 1}
, vec {68; 73; 68; 76; 0; 2; 113; 125; 4; 116; 101; 120; 116; 1} )

to_candid(data) OTOH, the former is a one argument (pair-typed) encoding, and there is no way to specify a type to get that thing out 😢


There might be a (theoretical) way to pull it out. If we allow interpreting the single-argument Candid-encoding as a record with index 0, then ?{_0_ : (Text, Nat)} would be the type assignment to give to from_candid.

ggreif avatar Jul 18 '23 20:07 ggreif

One solution might be to make from_candid take optional type parameters, and use the arity of the type parameters list to guide decoding. to_candid is deliberately an n-ary syntactic construct (so overloaded on arity) to make encoding work. I guess we didn't cover all cases testing decoding.

crusso avatar Jul 19 '23 11:07 crusso

Sounds great! So what you suggest is from_candid(<blob>; <arg-1-type>, <arg-2-type>, ..., <arg-N-type>)?

ggreif avatar Jul 19 '23 11:07 ggreif

from_candid already uses type annotations to guide decoding -- does @crusso's suggestion work that way already?

matthewhammer avatar Jul 19 '23 16:07 matthewhammer

Sounds great! So what you suggest is from_candid(<blob>; <arg-1-type>, <arg-2-type>, ..., <arg-N-type>)?

Almost.

What I was suggesting is

from_candid<t1,..,tn>(blob) : blob  -> ?(t1,...,tn)

(i.e. allow some optional type parameters)

Now you can specify the arity if needed.

So:

from_candid<(Int,Nat)> : blob -> ?((Int,Nat),) 

Not sure it would solve the problems with 1-tuples though which don't even exist in Motoko.... Sigh.

Here's a workaround that works by abusing Candid's defaulting option types:

  public query func decode2() : async ?(text : Text, nat: Nat) {
    let data: (Text, Nat) = ("text", 1);
    let blob = to_candid(data);
    switch (from_candid(blob) : ?((Text,Nat), ?())) {
      case null null;
      case (?(d, _)) ?d;
    }
  };

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

crusso avatar Jul 19 '23 17:07 crusso

Another (perhaps wacky) option that might work would be to have to_candid as a n-ary pattern on blobs that binds an nary sequence and fails to match on decoding:

<pat> := to_candid (<pat>,*)

example:

let to_candid (p : (Text, Nat)) = to_candid (("text", 0));

The length of the argument patterns sequence determines the arity and mirrors the introduction syntax.

Not pretty, but we could use constructor candid instead, though that name is likely to break some progs.

crusso avatar Jul 19 '23 17:07 crusso

I think everything is working as expected. I answered the same question on the forum: https://forum.dfinity.org/t/candid-and-tuples/17800/7

chenyan-dfinity avatar Jul 19 '23 20:07 chenyan-dfinity

No I think it's actually a bug, to_candid, which is syntax directed, is doing the right thing, but from_candid, which is only type directed, doesn't have enough info to distinguish between a singleton sequence containing a tuple and a tuple. I think. Using the pattern syntax or type parameters would help.

crusso avatar Jul 19 '23 22:07 crusso

Your trick of using a record to force the decoding is neat, but subtle.

crusso avatar Jul 19 '23 22:07 crusso

That's due to the semantic gap between Motoko and Candid. from_candid decodes multiple values instead of a single value. To decode singleton tuple, we will have to use record as specified in the Motoko-IDL spec.

chenyan-dfinity avatar Jul 19 '23 23:07 chenyan-dfinity

Given the cost of decoding, may using a pattern is a bad idea, in case people use the pattern in alternative cases etc...

crusso avatar Jul 20 '23 11:07 crusso

Your trick of using a record to force the decoding is neat, but subtle.

Only, it doesn't work because the top-level argument sequence is neither a tuple, nor a record in Candid. See test #4138.

EDIT: Wow indeed it works:

    let ?{_0_ = a; _1_ = b; _2_ = c} = from_candid(blob) : ?{_0_:Text; _1_: Nat; _2_: Bool};
    ?(a, b, c)

But OP's code isn't fixable the same way:

public query func decode() : async ?(text : Text, nat: Nat) {
    let data: (Text, Nat) = ("text", 1);
    let blob = to_candid(data);
    let ?{_0_ = d} = from_candid(blob) : ?{_0_ : (Text, Nat)};
    return ?d;
  };

But, this works:

public query func decode() : async ?(text : Text, nat: Nat) {
    let data: (Text, Nat) = ("text", 1);
    let blob = to_candid(data);
    let ?{_0_ = d; _1_ = f} = from_candid(blob) : ?{_0_ : Text; _1_ : Nat};
    return ?(d, f);
  };

I guess the non-tuple type under the option extracts the single arg, and it's components become the components of the tuple that we have put in. I'll write a test case for this if not yet present.

ggreif avatar Jul 20 '23 12:07 ggreif

I just double checked and the trick @chenyan-dfinity describes on the forum does seem to work (he's decoding a single argument sequence containing a triple as first argument to a triple.

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

crusso avatar Jul 20 '23 13:07 crusso