Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

from_candid() can't decode tuple types? #4130

Open
iclighthouse opened this issue Jul 18, 2023 · 13 comments · Fixed by #4138
Open

from_candid() can't decode tuple types? #4130

iclighthouse opened this issue Jul 18, 2023 · 13 comments · Fixed by #4138

Comments

@iclighthouse
Copy link

iclighthouse commented Jul 18, 2023

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


See below a few ways to accomplish what you want!

@ggreif ggreif changed the title from_candid() can't decode tuple types? from_candid() can't decode tuple types? Jul 18, 2023
@ggreif
Copy link
Contributor

ggreif commented Jul 18, 2023

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 ggreif added the Bug Something isn't working label Jul 18, 2023
@crusso
Copy link
Contributor

crusso commented Jul 19, 2023

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.

@ggreif
Copy link
Contributor

ggreif commented Jul 19, 2023

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

@matthewhammer
Copy link
Contributor

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

@crusso
Copy link
Contributor

crusso commented Jul 19, 2023

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
Copy link
Contributor

crusso commented Jul 19, 2023

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.

@chenyan-dfinity
Copy link
Contributor

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

@crusso
Copy link
Contributor

crusso commented Jul 19, 2023

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
Copy link
Contributor

crusso commented Jul 19, 2023

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

@chenyan-dfinity
Copy link
Contributor

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.

@crusso
Copy link
Contributor

crusso commented Jul 20, 2023

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

@ggreif
Copy link
Contributor

ggreif commented Jul 20, 2023

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.

@crusso
Copy link
Contributor

crusso commented Jul 20, 2023

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

ggreif added a commit that referenced this issue Jul 20, 2023
exercises the solution found in #4130
@ggreif ggreif removed the Bug Something isn't working label Jul 20, 2023
@mergify mergify bot closed this as completed in #4138 Jul 20, 2023
mergify bot pushed a commit that referenced this issue Jul 20, 2023
exercises the solution found in #4130
Resolves #4130.
@crusso crusso reopened this Feb 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants