dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

Reintroduce varargs parameter in functions (in limited capacity) #4088

Open hydro63 opened 2 months ago

hydro63 commented 2 months ago

I would like to reintroduce rest / varargs parameter (...rest) in functions, while also avoiding the problems that caused it to be removed.

I know that Dart use to have a varargs parameter, but it was removed because of the ambiguity between positional, optional, and named parameters. Those made it complex to implement and introduced ambiguity, about what value is binded to which argument.

The solution to these problems, is IMO limiting where can varargs parameters appear, making it impossible for a user to write ambiguous and still valid code. The varargs parameter would only be able to appear alone, not in combination with required, optional, or named parameters.

// only one valid
num sum(List<num> ...rest){
  ...
}

// invalid + every combination of them
num sum(List<num> ...rest, {bool alternate = false})
num sum(List<num> ...rest, [bool alternate = false])
num sum(num start, List<num> ...rest)

Argumentation

The reason i would like to reintroduce varargs parameters, is because sometimes we need to pass a collection of parameters to the function, but wrapping the parameters in List<T> is tedious and boiler heavy.

// example - simple parser combinator
// Function and(List<Function> funcs), Function parseSym(String sym), Function or(List<Function> funcs)
and([parseIdent, parseSym("="), parseExpr]);
and(parseIdent, parseSym("="), parseExpr);
// more complex
and([parseIdent, parseSym("="), or([parseLiteral, parseIdent])]);
and(parseIdent, parseSym("="), or(parseLiteral, parseIdent));

Also, introducing varargs in limited capacity would not impede on the current syntax, neither will limiting it's implementation limit it's use-cases. The only possible use-case of varargs parameters in current Dart is to pass a collection of constant parameters, without needing to constantly wrap them in lists. The varargs parameter used to have a use-case of passing optional parameters to a function, but that has already been supplanted by optional parameters.

As such, varargs parameter would only be able to appear as a single parameter, with no required, optional or named parameters. There would be no ambiguity in what values bind to which argument, because there would be only one sink. The compiler would also raise error when the developer tried to pass it a value of wrong type. The passed values would be collected and wrapped into a List<T> and given to the function.

Use-cases

The new varargs parameter could be used anywhere, where we only need to pass multiple values of the same type, but where it's expected that the developer is gonna pass them by one by one. Coincidentally, we can also find a use-case for it in Dart core library

While I understand the reasons for the removal of the varargs parameter from Dart, since they caused a lot of problems in combination with the other types of parameters, i also think that they still have a use-case that the other parameters can't supplant.

PS > it should also be possible to create the same behaviour with macros (the same as the Dart core example), but it would create a lot of boilerplate code, that's not needed

julemand101 commented 2 months ago

Sounds like a duplicate of: https://github.com/dart-lang/language/issues/1014 . Or at least strongly related to it.

lrhn commented 2 months ago

I don't believe Day has ever had varargs, the closest were an attempt to design them, which didn't pan out.

Specification-wise, it's not particularly hard. Positional parameters can end with […, ... List<int> args], which is a spread pattern. If we can use patterns for parameters otherwise, then a record spread pattern ...(int x, int y) && var point would match the next two positional arguments, and a spread list pattern would fit right in, but only for trailing positional parameters.

It would also match spread arguments like ... point or ... list.

A spread map pattern is more controversial, since parameters are not named with strings, and capturing argument names as symbols of a little too close to reflection. We also don't want to capture using a record of statically unknown type. So maybe there just shouldn't be unbounded named parameters.

hydro63 commented 2 months ago

@lrhn I don't see a reason to enable spreading some collection to bind multiple different arguments. The reason i allowed only varargs parameter, and no other combination of the different types, is because i know that people will try to get fancy with this otherwise, and wil create ambiguous code.

As i said, in current Dart there is no other valid use-case for varargs other than passing multiple constant parameters without having to wrap them in [].

Also, the reason i thought there was a varargs parameters is this: https://groups.google.com/a/dartlang.org/g/misc/c/0cQZpHs5D-g/m/NrKOuizLnqEJ

lrhn commented 2 months ago

Just adding [... List<Foo> foos] is an option. It will count as an optional positional parameter, in that it cannot be used together with named parameters. The [...] wrapper is there to show that.

If we have var-arg parameters, we also want var-arg arguments. That means a call like doFoo(42, "a", Foo(0), ...foos).

We can stop there. It is a well-defined self-consistent feature.

The reason I want to combine it with parameter patters is that it looks like one. Or more precisely, it looks similar to the record spread argument/record spread paramters that I would want together with pattern parameters.

Plain pattern parameters are just patterns on each position: void foo(int x, String y, [Foo first, Foo second, ... List<Foo> rest]). "Spread" patterns would match multiple arguments: Box box(...(double x, double y) min, ...(double x, double y) max) => Box(bottomLeft: min, topRight: max); which takes four arguments, but match them into records already in the parameter list, and you can call it as box(...minPoint, ...maxPoint) to spread the records into the argument list.

That's what ... List<int> varargs does too, takes (the rest of) arguments and combine them into a single list during the "binding actuals to formals" step of invoking the function. So it would make sense to combine the features into one, rather than designing them separately, and risking small differences in behavior when combining the two features.

(When @munificent says that we had var-args in Dart for a while, it wasn't a feature that was ever released in a stable version. It was being worked on, but it didn't ship, for the reasons he gave. Looking from the inside, they were in Dart for a while, but from the outside, that wasn't visible.)

hydro63 commented 2 months ago

Actually, i think that the feature i want (varargs where there are multiple constant parameters) could be done by using macros.

// before
@Varargs(int, "values")
int sum(){
  return values.fold(0, (a,b) => a+b);
}

// after applying the macro - creates optional variables, and wraps them in list
int sum(int values_1, [int? values_2, int? values_3, ..., int? values_n]){
  final values = [values_1, values_2, values_3, ..., values_n].where((e) => e != null).toList<int>();
  return values.fold(0, (a,b) => a+b);
}

I don't know if it's a good reason to stop the discussion about supporting varargs, just putting it out there.

Edit

I tried to write the macro, and found out that you can't change the function signature with macros (meaning no new arguments). That is a correct decision in the grand scheme of things, but in this case it's actually needed and also reasonable to change the signature.

Oh well, the macros solution doesn't work.

TekExplorer commented 1 month ago

I'd argue that that particular example is better served with a collection extension:

extension IterableSum on Iterable<int> {
  int sum() => fold(0, (a, b) => a + b);
}

which, afaik, already exists in the collection package (could be wrong)

TekExplorer commented 1 month ago

That said, i think I'd rather get a tangential-yet-similar feature of receiving any set of parameters in the shape of a record to be passed into the "actual" function, wrapper style.

One might argue "macros!" but no, because suddenly I'd have a million subtypes that would have been perfectly represented via generics.

Record spread + record args (though optional positional, optional named, and defaults don't have representation...) would give much more power to generic functions.

There's probably already an issue for that, but I otherwise don't see the point of varargs, when an optional list fits that fine. its 2 extra characters, and you get all collection features, like collection-for and collection-if, etc.

Theres also no way to define a generic-extends-function that declares a desired return type while letting the user determine the parameter shape. (perhaps <R extends Record, F extends ReturnType Function(...R)>? idk.) that also might have an issue somewhere, but idk.