`from_candid()` can't decode tuple types?
Example: https://m7sm4-2iaaa-aaaab-qabra-cai.ic0.app/?tag=3732396148
See below a few ways to accomplish what you want!
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.
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.
Sounds great! So what you suggest is from_candid(<blob>; <arg-1-type>, <arg-2-type>, ..., <arg-N-type>)?
from_candid already uses type annotations to guide decoding -- does @crusso's suggestion work that way already?
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
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.
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
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.
Your trick of using a record to force the decoding is neat, but subtle.
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.
Given the cost of decoding, may using a pattern is a bad idea, in case people use the pattern in alternative cases etc...
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.
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