Solidity: Allow multi-variable declaration for tuple assignment to use explicit types.

Created on 15 Feb 2018  路  43Comments  路  Source: ethereum/solidity

With the deprecation of var, it is not possible anymore to easily assign the result of a function to local variables. We should allow types to be specified in such assignments:

(unit a, uint b, SomeStruct c) = f();
annoys users feature

Most helpful comment

I just found the solution (credits to: https://ethereum.stackexchange.com/questions/21348/get-the-return-of-a-function-with-multiple-returns)

  uint256 c;
  uint256 d;
  (c, d) = f();

The following is also supported in case you're only interested in d :

  uint256 d;
  ( , d) = f();

All 43 comments

Are the surrounding parentheses necessary?

Not sure, we always had them to make the tuples explicit.

Also it might be confusing for people coming from C or JavaScript (because there, , is almost like ; and thus the assignment in the above example would only apply to c).

With the deprecation of var, it is not possible anymore to easily assign the result of a function to local variables.

This is not true, it is only true in conjunction with the disallow-uninitalised-storage-pointers and only affects types/variables residing int storage.

Probably a better example would be one using such a case.

I would say that

uint a;
uint b;
uint c;
(a, b, c) = f();

already counts as "not easy".

Also it might be confusing for people coming from C or JavaScript

Yeah, I'm coming from Ruby where this is valid:

a, b = [1, 2]

This post was exactly what I was looking for! In all the Ethereum examples, the "var" keyword is used when you want to assign values to a tuple function (e.g. var (x,y) = f() ). Since "var" is now deprecated, how should a multi variable assignment be done?

I just found the solution (credits to: https://ethereum.stackexchange.com/questions/21348/get-the-return-of-a-function-with-multiple-returns)

  uint256 c;
  uint256 d;
  (c, d) = f();

The following is also supported in case you're only interested in d :

  uint256 d;
  ( , d) = f();

To be clear:

The grammer of a tuple expression should consequently be changed from

TupleExpression = '(' ( Expression? ( ',' Expression? )* )? ')' | '[' ( Expression ( ',' Expression )* )? ']'

to

TupleExpression = '(' ( (Expression, VariableDeclaration)? ( ',' (Expression, VariableDeclaration)? )* )? ')' | '[' ( Expression ( ',' Expression )* )? ']'

?

No, probably not. I guess it makes more sense to change VariableDefinition, resp VariableDeclaration to support tuple types instead, since tuples of variable declarations are no expressions...

So changing

VariableDefinition = ('var' IdentifierList | VariableDeclaration) ( '=' Expression )?

to

VariableDefinition = ('var' IdentifierList | VariableDeclaration | VariableDeclarationList) ( '=' Expression )?
VariableDeclarationList = '(' ( VariableDeclaration? ',' )* VariableDeclaration? ')'

is probably more like what we want?

Should the assignment be required for multi-variable declarations or should

(uint a, uint b, SomeStruct c);

be valid as well?

If the assignment should be required, that would probably mean

VariableDefinition = ( ( ('var' IdentifierList | VariableDeclaration) ( '=' Expression )? ) | (VariableDeclarationList '=' Expression) )
VariableDeclarationList = '(' ( VariableDeclaration? ',' )* VariableDeclaration? ')'

A potential option would be:

SimpleStatement = VariableDefinition | TupleDefinition | ExpressionStatement
VariableDeclarationList = '(' ( VariableDeclaration? ',' )* VariableDeclaration? ')'
TupleDefinition = VariableDeclarationList '=' Expression

@axic just said we should postpone and further discuss this, though.

I would say that assignment is required, otherwise it is just weird, even if this adds a special case.

I am not fully convinced this is a problem to be solved.

One benefit of not having it is the visual clarity that new variables are declared, whereas in a tuple declaration they would be less visible.

It is kind of the same discussion whether we should have multiple variable declarations on the same line or not. Currently we do not support that.

So, so far I think we had the following options:

Not support variable declarations in tuple assignments at all:

uint a;
uint b;
uint c;
(a, b, c) = f();

Pro: declarations are visually clear
Con: verbose; dangling pointers; unnecessary initialization (without optimization)

Support it using parentheses:

(uint a, uint b, uint c) = f();

Pro: single line; immediate assignment without pre-initialization
Con: visually unclear that declarations were made

Support it without parentheses:

uint a, uint b, uint c = f();

Pro: same as last one + visually clearer that declarations were made
Con: may look like assignment only to c; may be weird if not possible without assignment

I'm voting for (uint a, uint b, uint c) = f(), unless we find some better alternative.

@axic this is a problem that needs to be solved, many people are annoyed by the fact that we removed var-tuple assignments. The language does not get safer by just removing features, we have to provide viable alternatives.

I can understand @axic's concern about visually hiding declarations. And I also agree with @axic, that we should think about whether we should allow multiple variable declarations on the same line in general, if we choose to allow it when assigning.

What about (uint, uint, uint) (a,b,c) = f()? I'm not sure about it myself, but it would be closer to the old var syntax and make it at least somewhat clearer that a declaration has taken place. Again, declarations without assignment should be considered as well, though.

One should probably also consider #3314 and whether e.g. (uint ,_ , uint) (a,_,c) = f() or (uint,,uint) (a,,c) = f() will be acceptable (I'd suggest to disallow them, but to allow (uint, uint, uint) (a,,b) = ..., resp. (uint, uint, uint) (a,_,b) = ...).

Another general consideration from a language design perspective would be: do tuple expressions have a type? As far as I can see, currently there is no tuple type in the grammar (although there's TupleType in Types.h) - however, tuples can still be returned and assigned, making it hard to conceptualize the types in statements like return (0,0); or (a,b) = (0,1);

So should tuples maybe be a full-fledged kind of type in general? (uint, uint, uint) would then be a tuple type and could be used in variable declarations as well as in assignments. One could still impose the restriction that expressions of the tuple type cannot be bound to a single variable name, as in (uint, uint, uint) t;, since otherwise one would also have to introduce some means to access tuple elements (on the other hand doing so may not be the worst of ideas). However, even if one only allows tuples to be bound to several variables in tuple expressions such as (a,b,c), this would still mean that one should at least allow a declaration like (uint, uint, uint) (a,b,c); in this case.

Also, consider assigning into a singleton tuple (whether to allow or not).

(uint x,) = f();

@ekpyron I think (int a, int b) = X; can appear without (int a, int b). The first is desired just because X is a tuple.

I think singleton tuples and tuple types are out of scope for this discussion. They are currently both possible, the latter is just not nameable. We certainly should disallow catch-all tuple components, but that is also addressed in a different issue.

While (uint, uint, uint) (a, b, c) looks good from a language theoretic viewpoint, I don't think it is a usability improvement over (uint a, uint b, uint c), as it has almost the same drawback and creates a new concept that needs to be learnt by users (they already know (uint a, uint b, uint c) from function headers).

And further in the future, substitute (uint a, uint b, uint c) = f() into g(uint a, uint b, uint c) and obtain g f().

Can we have some actual examples where this feature is badly needed?

@chriseth I'm not sure whether (uint a, uint b, uint c) is less of a new concept than (uint, uint, uint) (a,b,c)...
To me function headers are quite different - function f() returns (uint a, uint b, uint c) { (a,b,c) = (1,2,3); still requires to mention (a,b,c) again, whereas function f() returns (uint a, uint b, uint c) { return (1,2,3); } is the same as function f() returns (uint, uint, uint) { return (1,2,3); }...

However (uint, uint, uint) (a,b,c) = ... is practically the same as var (a,b,c) = ..., just with explcit types for safety...

@axic Ultimately it's a convenience feature, so I don't think there's cases in which it is really badly needed... However, I think it may be worth considering, whether promoting tuple types to first-class types that can actually be named rather than only having them somewhat implicitly in assignments and function returns, wouldn't be cleaner in general...

Yeah, it's probably true that this is only badly needed in cases where the types cannot be named anyway.

Ah right, if one of the variables is a storage pointer, it might be very hard to refactor the warning away, and it might require multiple storage reads.

If we introduce tuple types, I would probably prefer somethnig like uint*uint*uint (a, b,c) = ... - but we should check how other languages did this.

In general, I still think that most solidity users would find (uint a, uint b, uint c) = ... more natural, but we can make a survey.

I think somewhat relevant is the discussion on how restricted variable declarations are at the moment: they do not allow multiple declarations (with or without value assignment) in the same statement.

If those are like that, why is tuple declaration an exception?

I do not mean any of this is good or bad, but worth looking at the bigger picture.

I don't see the difference between a tuple declaration and the declaration of multiple variables in the same statement. You can declare multiple variables in the same statement, you just need to add parentheses.

You can declare multiple variables in the same statement, you just need to add parentheses.

What do you mean by that? Do you have an example?

Of course you need var because I was too lazy to change the parser accordingly back in the days: var (a, b, c, d).

Since var is deprecated, would you say that it should be made possible without var?

Yes, exactly. It should be possible to assign function return values to newly declared variables because a variable declaration with assignment looks safer (and probably is) than without.

Not talking about function return values, but actual variable declarations.

Yes, the declaration of multiple variables in the same statement without assigning values is redundant and can be removed.

For me this feature is a plus, because I can follow the single assignment style.

Without this feature, I have to write

<declarations of a and b>

<around here a and b contain the default value that's never needed (BAD)>

(a, b) = f();

With this feature, I can write

(uint a, uint b) = f();

So, each of a and b receives one value ever.

Expanded the only case zeppelin does multi return values into the options discussed above:

contract C {
  function tokenGrant(address _holder, uint256 _grantId)
    constant
    returns (
      address granter,
      uint256 value,
      uint256 vested,
      uint64 start,
      uint64 cliff,
      uint64 vesting,
      bool revokable,
      bool burnsOnRevoke
    )
  {
  }

  function f() {
    address granter;
    uint value;
    uint vested;
    uint start;
    uint cliff;
    uint vesting;
    bool revokable;
    bool burnsOnRevoke;

    // note: implicit conversion of start/cliff/vesting from 64 to 256bit
    (granter, value, vested, start, cliff, vesting, revokable, burnsOnRevoke) = tokenGrant(0, 0);
  }

  function g() {
    // note: implicit conversion of start/cliff/vesting from 64 to 256bit
    (address granter, uint value, uint vested, uint start, uint cliff, uint vesting, bool revokable, bool burnsOnRevoke) = tokenGrant(0, 0);
  }

  function h() {
    // note: implicit conversion of start/cliff/vesting from 64 to 256bit
    address granter, uint value, uint vested, uint start, uint cliff, uint vesting, bool revokable, bool burnsOnRevoke = tokenGrant(0, 0);
  }

  function i() {
    // note: implicit conversion of start/cliff/vesting from 64 to 256bit
    (address, uint, uint, uint, uint, uint, bool, bool) (granter, value, vested, start, cliff, vesting, revokable, burnsOnRevoke) = tokenGrant(0, 0);
  }
}

Most of the use-cases will probably cease being use-cases when we have support for structs...

Discussion: let's go with the original proposal for now and try to make breaking releases more often.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AnthonyAkentiev picture AnthonyAkentiev  路  3Comments

chriseth picture chriseth  路  4Comments

chriseth picture chriseth  路  4Comments

chriseth picture chriseth  路  3Comments

Dohtar1337 picture Dohtar1337  路  4Comments