Language: Pass indexes to List iteration methods like forEach fold etc.

Created on 13 Aug 2020  路  12Comments  路  Source: dart-lang/language

Currently none of the iteration methods for lists accepting a callback are passing the current items index to it.
So you have to manually call indexOf.

For example in List.fold which is currently:

List<T>.fold<R>(R, Function(R, T))

Could be:

// Pass index of each element while iterating
List<T>.fold<R>(R, Function(R, T, int))

Is there a specific reason why this is not supported?

feature

Most helpful comment

Dart does not have a type system which allow you to provide either a function taking one argument or a function taking two arguments (unlike, say, JavaScript).
The two operations would have to be two different methods. It was not considered worth the API clutter to have both operations, and the one without the index was vastly more used.

It's unlikely that we'll change the Iterable API now.
It is possible to add such methods using extension methods, or if we ever get interface default methods.

All 12 comments

Can't speak on why it wasn't initially included, but this could be implemented using extension methods, perhaps named foldIndexed. I've done similar with all the other methods as well.

Something like:

extension IterUtils<T> on Iterable<T> {
  T foldIndexed(T initialValue, T Function (T, T, int) combine) {
    var i = 0;

    var value = initialValue;
    for (final item in this)
      value = combine(value, item, i++);

    return value;
  }
}

void main() {
  final result = [1,2,3].foldIndexed(0, (lhs, rhs, i) => lhs + rhs + i);
  print(result);
}

Dart does not have a type system which allow you to provide either a function taking one argument or a function taking two arguments (unlike, say, JavaScript).
The two operations would have to be two different methods. It was not considered worth the API clutter to have both operations, and the one without the index was vastly more used.

It's unlikely that we'll change the Iterable API now.
It is possible to add such methods using extension methods, or if we ever get interface default methods.

What about method overloading? If that does happen, could the method used depend on the function argument arity? e.g.

T fold<T>(T initialValue, T combine(T previousValue, E element)) {
  var value = initialValue;
  for (E element in this) value = combine(value, element);
  return value;
}

T fold<T>(T initialValue, T combine(T previousValue, E element, int)) {
  var i = 0;
  var value = initialValue;
  for (E element in this) value = combine(value, element, i++);
  return value;
}

Yes, with method overloading, it would be less invasive to have multiple versions of the same function. We wouldn't have to name one of them foldIndexed.

Still, without interface default methods, we probably won't dare change the Iterable interface, so I think it would require both for us to add fold-with-index to Iterable.

Interesting, link to interface default methods for those interested.

Dart does not have a type system which allow you to provide either a function taking one argument or a function taking two arguments (unlike, say, JavaScript).
The two operations would have to be two different methods. It was not considered worth the API clutter to have both operations, and the one without the index was vastly more used.

It's unlikely that we'll change the Iterable API now.

@lrhn Honestly I find that pattern of multiple different methods for different number of arguments bad ( forEach, forEachIndexed).

For the following reasons:

  1. it adds complexity to the api ( more methods ), while you can have just one that does the job well in both scenarios.
    It adds more complexity because you have to search for a method that does what you ask, while you could just call intellisens on the function itself to see if there is any additional arguments that might fits your needs.

    1. I see libraries do things like combineLatest2(a, b, fn); combineLatest3(a, b, c, fn); They could have used a list but at some point people want to be as close as possible as libraries they are trying to port.

    2. it's just easier to read than a list sometimes. eg: print(a, b, c);

Having said that, I would like function to be able to receive more arguments that they asked for so that things like this would be possible

Couldn't it be ?

  void forEach(void f(E element, [int index])) {
    for (int index = 0; index < length; index ++) {
       f(element, index) ; // where this would compile
    }
  }

and the dart compiler allow the anonymous function to receive an argument it didn't ask for ? So both forEach((el).. and forEach((el, i) would work ?

Imo the benefits are:

  • allow improvements in api without making breaking changes. EG you could add a functionality (here index in forEach) without having existing code break.
  • compatibility with lots of js libraries
  • no more multiple functions for different arguments

@cedvdb There are two problems needed to be solved to allow forEach to accept both void Function(E) and void Function(E, int) as arguments.

The first one is that those two function types don't have a supertype more precise than Function, so in order to allow you to pass the arguments at all, you'd lose all type information for the parameter. The type void Function(E, [int]) is not a supertype, it's a subtype, so using that as parameter type would mean that you couldn't use either of the simpler function types, you must always pass a function with an optional second argument.

Second, there is no simple way to pass an extra argument to a function which does not expect it. We could allow you to "overapply" function invocations, but that's also a very good way to hide errors. It would at least require a special syntax, say action(element, ?index) which would only pass index if the action function accepts a second argument.
However, you can do that with type checking:

if (action is void Function(E, int)) {
  action(element, index);
} else if (action is void Function(E)) {
  action(element);
} else {
  throw ArgumentError("action bad, mkay!");
}

The biggest problem is the former, that there is no type for either void Function(E) or void Function(E, int). If Dart had union types, you could use those. I think I'd be happier with adding overloading than with adding union types. The latter is very costly in complexity, the former ... slightly less so.

In any case, I've added forEachIndexed as an extension method on Iterable in package:collection. It'll be released along with null safety.

I understand that there are complexities involved. However, as an end user, I'm not thinking about the internal complexities.

My remarks were to argue for a better end result than forEachIndexed which I think is sub par. I'm beginning to see this pattern a lot.
That might be complex to implement but that's outside the scope of my knowledge about dart as a relatively new user of the language / tool.

I still think that the true solution to this problem would be to allow assigning a closure with fewer parameters than the type requires

I originally suggested that void Function() should be assignable to void Function(int), but that was a bad idea.

But an alternative is to make a tear-off, like with extensions tear-off

When writing:

[].forEach(function);

void function() {}

that would be syntax sugar for:

[].forEach((item, index) => function());

void function() {}

I think that's an important feature, because this allows packages authors to add extra parameters to the callback without it being a breaking change.
It's also a very common pattern to move the more "niche" parameters as the very last parameters of the closures, like with ValueListenableBuilder and its child.

However, as an end user, I'm not thinking about the internal complexities.

If there are cases where the language can encapsulate the complexity, then I agree with your point here. For example, type inference in Dart can get pretty subtle, but it does so in ways users rarely notice, so that complexity is relatively tolerable. It still has a cost in that some users will hit it and need to understand what's going on. There is also always the opportunity cost. Time the Dart team spends implementing complex feature A is time not spent on other features that might be more helpful to users.

In the example here, I don't think the language could easily hide the complexity. Things like function types and method overloading are fundamental to the user experience of the language. Adding a new method with a different name is a simple solution that is also easy for users to understand. Being able to overload methods or somehow changing the type rules around function types is a lot more mechanism to add to the language and to burden our users with. It's not clear to me that it carries its weight.

Maybe another thread is needed because it's diverging from the original intent into a wider request.

I think that's an important feature, because this allows packages authors to add extra parameters to the callback without it being a breaking change.

The thing is that a library owner cannot deal with breaking changes himself. You might end up with libraries do things like doSomething(f(int a)), doSomethingV2(f(int a, int b)), doSomethingV3(f(int a, int b, int c)) as a library evolves and is afraid of breaking changes.

Some propositions, for food for thought:

  1. even though not ideal because it's less strictly typed: like it works in typescript, the function() {} would adhere to the typing of any additional parameters.

For example in typescript, you can do this:

type A = (a: number, b: number) => void;
const a: A = () => { } // because it has less params

allowing for the forEach to receive a function with less parameters

forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;

It just ignores those additional params. Even though it's less strict I don't see why it has to be strict in this case. If you don't need the parameters, why would you have to define them ?

IE:

aWidget(
  aCallback: (_, __, ___) => print('callback called');
);

in the example above I don't use the parameters at all, why should I define them ?

  1. A bit more strict / manual, we could have some way to say "this is a type function with x, y, z parameters that are optional, any implemetation of this type, can receive those 3 parameters without defining them". Effectively the same as 1. but you have to opt in with a syntax. I would argue that if the function doesn't define an optional parameter it should be ok.

EG:

Where for the dart forEach implementation we'd have this:

  void forEach(void f([E element, int index])) {
    for (E element in this) f(element, index); 
  } 

which would not throw an error when used with this:

[].forEach(() {})
  1. Overloading. I'm not a fan of this solution because every time you have a callback as parameter it becomes very verbose if you want to handle all cases.

@rrousselGit

Your example isn't very clear. Do you mean that

[].forEach(() {});
[].forEach((item) {  });
[].forEach((item, index) {  });

that would be syntax sugar for:

[].forEach((item, index) => () {});
[].forEach((item, index) => (item) {  });
[].forEach((item, index) => (item, index) {  });

I don't see how this would help, you'd still need overloading or an union type, to type the different callbacks, doesn't it ?

There is also always the opportunity cost.

Agreed. I guess you are ordering priorities by what's more important to the community. I personally see this as crucial as very annoying.

Your example isn't very clear. Do you mean that

[].forEach(() {});
[].forEach((item) {  });
[].forEach((item, index) {  });

that would be syntax sugar for:

[].forEach((item, index) => () {});
[].forEach((item, index) => (item) {  });
[].forEach((item, index) => (item, index) {  });

I don't see how this would help, you'd still need overloading or an union type, to type the different callbacks, doesn't it ?

Yes I do mean that.

No, you wouldn't need overloading/unions/to type the different callbacks.

forEach wouldn't take a void Function(T) anymore but a void Function(T, int).
But code that do .forEach((item) {}) would still compile

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jonasfj picture jonasfj  路  3Comments

mit-mit picture mit-mit  路  3Comments

munificent picture munificent  路  5Comments

har79 picture har79  路  5Comments

creativecreatorormaybenot picture creativecreatorormaybenot  路  3Comments