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

Allow spread operator in list constructors #52

Closed
jclark opened this issue Apr 1, 2019 · 20 comments
Closed

Allow spread operator in list constructors #52

jclark opened this issue Apr 1, 2019 · 20 comments
Assignees
Labels
Area/Lang Relates to the Ballerina language specification sl-update-priority Priority for Swan Lake Updates status/pending Design is agreed and waiting to be added Type/Improvement Enhancement to language design

Comments

@jclark
Copy link
Collaborator

jclark commented Apr 1, 2019

JavaScript allows spread operator ... not only in function calls but also in list constructors. We should allow this also. It also allows the spread operator to refer to anything iterable, not just a list.

At the moment it when applied to a list, it has the effect of specifying each member as a positional argument; I wonder if it could also be made to apply to a record with the effect of specifying each member as a named argument.

JavaScript also allows multiple occurrences of the spread operator in a single argument list.

@jclark jclark added Type/Improvement Enhancement to language design Area/Lang Relates to the Ballerina language specification status/idea An idea that is not yet a worked-out proposal labels Apr 1, 2019
@jclark jclark added this to the 2019Rn milestone Apr 17, 2019
@jclark
Copy link
Collaborator Author

jclark commented Apr 19, 2019

I think at least allowing ...x in a list constructor or tuple constructor expression is straightforward and a prerequisite for #2 (since arg-list allows ...x). @sanjiva Do you agree? If so, we should separate this part out, and do at the same time as #2.

@sanjiva
Copy link
Contributor

sanjiva commented Apr 21, 2019

If we put this into a tuple constructor the length of the tuple would have to be computed at runtime. You had proposed a terminology change to tuple constructor to be referred to as fixed length constructor .. would (1, 2, ...x) be a fixed length constructor?

It works for arg-list because the resulting value is received in the function as a rest arg. However, the current draft has this sentence "The rest-arg is not restricted to supplying the part of the argument list that will be bound to a rest-param" which I don't grok .. and I looked at the rest-param definition and its not clear how that would work.

@jclark
Copy link
Collaborator Author

jclark commented Apr 21, 2019

Given that we are saying that a tuple constructor (or whatever we decide to rename it to) creates a fixed length value, then ...x in a tuple constructor would only be allowed when x is a fixed length type.

As regards your second para, functions don't receive a rest arg as a distinct thing: they receive a single list with all their args (although it is not reified as a list); this list is not necessarily fixed length. (This is described in the function types section.) With T... allowed in tuples, this argument list can be typed as a tuple type.

One result of all this is that ...x can do apply in a type-safe way:

function foo(int x, string...) { }
type FooArgs (int, string...);

function bar(FooArgs fa) {
  foo(...fa);  // i.e. apply(foo, fa);
}

@sanjiva
Copy link
Contributor

sanjiva commented Apr 22, 2019

Ack and +1 to allowing ...x in tuple constructors.

To call bar() I'd have to say:

bar ((10, "a", "b", "c"));

as bar() takes a single arg of a tuple type. So this is not a very useful example but in general being able to take a tuple value and spread it into an arg list of a function is very nice.

What this means is that param-list of a function is really a way to declare a tuple giving names to the slots so one can access them differently from indices. It feels to me like we should generalize that to all tuples ..

@jclark
Copy link
Collaborator Author

jclark commented Apr 22, 2019

On your last para, a tuple-binding-pattern effectively does that. You can think of the parameter list of a function as doing a destructuring assignment on the argument list.

@jclark
Copy link
Collaborator Author

jclark commented May 28, 2019

We should also allow spread in mapping constructors applied to mapping values, e.g.

{ ...x }

This is very useful if you want to make use of something that you have matched in a binding pattern with ...x.

@jclark jclark modified the milestones: 2020R2, 2020R1 Jan 23, 2020
@jclark jclark changed the title Generalize spread operator Allow spread operator in list constructors Jan 23, 2020
@jclark jclark removed the status/idea An idea that is not yet a worked-out proposal label Feb 24, 2020
@jclark jclark modified the milestones: 2020R1, 2020R2 Feb 24, 2020
@jclark jclark added the status/pending Design is agreed and waiting to be added label Aug 15, 2020
@lochana-chathura
Copy link
Member

I'm working on the implementation. Consider the following scenario.

string[] a1 = ["s"];

[string, string, string...] a2 = [...a1, ...a1]; // @output ["s","s"]
[(int|string), string, string...] a3 = [...a1, ...a1]; // @error no filler value for type '(int|string)'
[string, int...] a4 = [...a1]; // @error incompatible types: expected 'int', found 'string'

Is the above the expected behavior? Or should a2 result in an error? I was taking the filler values into account.

@jclark
Copy link
Collaborator Author

jclark commented Mar 24, 2022

I agree a3 and a4 are errors.

I hadn't considered how spread interacts with filler values, but I think you are right to take them into account.

So then

string[] a1 = [];
string[] a2 = ["s"];
[string, string, string...] a3 = [...a1,...a2]; // @output ["s", ""]

Right? That seems reasonable.

@MaryamZi
Copy link
Member

Wouldn't allowing this mean that checking and adding the unspecified (fill-able) members is a responsibility of the runtime list value creation, rather than something the compiler can desugar? Because we don't know at compile-time if we have to use filler values and/or for which members we have to use filler values?

jBallerina actually checks the length and fills values at runtime atm, but before this change I was thinking it could probably be handled at compile-time.

@lochana-chathura
Copy link
Member

lochana-chathura commented Mar 28, 2022

type checking gets more complicated for a scenario like this,
because we have a varying length list in the middle that overlaps with the fixed members P, Q, R, S, and T.

    X[] b = [];
    [Y, Z...] f = [y];

    [P, Q, R, S, T, U...] a = [
        ...b,
        c, // should be compatible with types P, Q, R, S, T, and U
        d, // should be compatible with types Q, R, S, T, and U
        e // should be compatible with types R, S, T, and U
        ...f,
        g, // should be compatible with types T and U
        h // should be compatible with types U
    ];

is it worth implementing? Maybe we better off saying, varying length lists with spread operator are allowed only for the rest-descriptor, considering #52 (comment) as well.

@jclark
Copy link
Collaborator Author

jclark commented Mar 28, 2022

varying length lists with spread operator are allowed only for the rest-descriptor

That's more restrictive than for ... in argument lists. I suggest: spread operator with varying-length list is allowed only for last member of list constructor.

@jclark
Copy link
Collaborator Author

jclark commented Mar 29, 2022

On further thought, I suggest: spread operator with varying-length is allowed in two cases:

  1. the spread operator is in the last member of a list constructor [...x] or [a, b, ...x], or
  2. the inherent type of the list being constructed has no required members int[] v = [...x, ...y];

@lochana-chathura
Copy link
Member

lochana-chathura commented Mar 29, 2022

I think we can relax allowing spread operator with varying length for inherent types having required members a little further.

As long as we can guarantee that the required members are fulfilled, we can allow as many as spread members with varying lengths.
e.g.

int[] a1 = [];
[int, string, any...] a2 = [1, "s", ...a1, ...a1, 23, "x", ..a1]; 

[int, string] a3 = [1, "s"];
[int, any, anydata...] a4 = [...a3, ...a1, true, ...a1, "x"]; 

[int, string, string] a5 = [1, "s"];
[int, any, anydata...] a6 = [...a5, ...a1, true, ...a1, "x"]; 

In the above scenarios, required members are guaranteed to have a value.

In a nutshell, we do the last member restriction, only when the required members are not guaranteed to have a value.
e.g.

[int, string] a7 = [1, "s"];
[int, any, int, anydata...] a8 = [...a7, ...a1];  // Here only the last member is allowed to be having a varying length

@lochana-chathura
Copy link
Member

So I suggest: the spread operator with varying-length is limited to last member only if the inherent type of the list being constructed has required members that are not guaranteed to have a value. Allowed anywhere otherwise.

@jclark
Copy link
Collaborator Author

jclark commented Mar 29, 2022

I would like to address Maryam's point too.

@lochana-chathura
Copy link
Member

I would like to address Maryam's point too.

Then I think we have to disallow the following.

string[] a1 = [];
string[] a2 = ["s"];
[string, string, string...] a3 = [...a1,...a2]; // @output ["s", ""]

So in summary:
the spread operator with varying-length is not allowed if and only if the inherent type of the list being constructed has required members that are not guaranteed to have a value.

@jclark
Copy link
Collaborator Author

jclark commented Mar 29, 2022

I think that's OK. We need to make "guaranteed to have a value" precise, so I would phrase it something like:

If the expression for a list member is ...x and the static type of x is not fixed length, then the minimum total number of list members from this and previous expressions must be greater than or equal to the number of members required by the inherent type of the list being constructed.

@jclark
Copy link
Collaborator Author

jclark commented Apr 1, 2022

Should also make this work for table constructors.

Edited: this is #1102

@lochana-chathura
Copy link
Member

We need to also finalize the limitations with the spread operator when there is no contextually expected type for the list.

As per the current implementation with 2201.1.0 release, var inference is not allowed with spread operator expression having varying length list. But, we can relax it with future releases.

(int|string)[] x = [1, 2];
var _  = [0, ...x]; // not allowed atm.

["s", int...] y = ["s", 2];
var _  = [0, ...y]; // not allowed atm.

We can infer the type for the above as a tuple type having a rest descriptor; [int, (int|string)...] and [int, "s", int...] respectively.

I suggest the limitation as follows,
For var inference with lists, only one spread member having a varying length list is allowed and it has to be the last member.

@jclark
Copy link
Collaborator Author

jclark commented May 9, 2022

We need to also finalize the limitations with the spread operator when there is no contextually expected type for the list.

I don't see why there should be limitations. The issue is what type you infer for the constructed list, when you have a varying length spread member that is not the last member of the list constructor. The obvious answer is that this member and all subsequent members turn into a T... where T is the union of all the possible member types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area/Lang Relates to the Ballerina language specification sl-update-priority Priority for Swan Lake Updates status/pending Design is agreed and waiting to be added Type/Improvement Enhancement to language design
Projects
None yet
Development

No branches or pull requests

4 participants