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

Add an example showing the "lazy eval" aspects of map() to 'Iterable collections' page #5619

Open
1 task
dtonhofer opened this issue Mar 3, 2024 · 2 comments
Labels
a.tut.codelab Relates to codelabs hosted on dart.dev. e1-hours Can complete in < 8 hours of normal, not dedicated, work fix.examples Adds or changes example from.page-issue Reported in a reader-filed concern p2-medium Necessary but not urgent concern. Resolve when possible. st.triage.ltw Indicates Lead Tech Writer has triaged

Comments

@dtonhofer
Copy link

Page URL

https://dart.dev/codelabs/iterables/

Page source

https://github.com/dart-lang/site-www/tree/main/./src/content/codelabs/iterables.md

Describe the problem

At mapping, we read:

map() returns a lazy Iterable, meaning that the supplied function is called only when the elements are iterated.

Suggesting to add an example showing the "on-demand" aspect of this evaluation, in particular that even previously computed mappings may be recomputed (unless the implementation cacches them). Attention needs to paid to immutability of the mapped structure.

Expected fix

Example code:

void main() {
  const List<int> myList = [1, -2, 3, 42];
  // Run tests based on modifiable lists created
  // by shallow-copying myList:
  testOne(List.of(myList));
  testTwo(List.of(myList));
}

void testOne(List<int> list) {
  // Build iterable:
  final numbersByTwo = list.map((number) {
    print('2 * $number');
    return number * 2;
  });
  // Modify the underlying list *before* querying iterable:
  list[0] = 10;
  // The resulting iterable has picked up the change:
  assert(numbersByTwo.elementAt(0) == 20 &&
      numbersByTwo.elementAt(1) == -4 &&
      numbersByTwo.elementAt(2) == 6 &&
      numbersByTwo.elementAt(3) == 84);
  print('Numbers x 2 after change: $numbersByTwo');
}

void testTwo(List<int> list) {
  // Build another iterable:
  final numbersByThree = list.map((number) {
    print('3 * $number');
    return number * 3;
  });
  // Results are as expected:
  assert(numbersByThree.elementAt(0) == 3 &&
      numbersByThree.elementAt(1) == -6 &&
      numbersByThree.elementAt(2) == 9 &&
      numbersByThree.elementAt(3) == 126);
  // Consult iterable by printing it:
  print('Numbers x 3 before change: $numbersByThree');
  // Modify the underlying list *after* consulting iterable:
  list[0] = 0;
  // The resulting iterable has picked up the change:
  assert(numbersByThree.elementAt(0) == 0 &&
      numbersByThree.elementAt(1) == -6 &&
      numbersByThree.elementAt(2) == 9 &&
      numbersByThree.elementAt(3) == 126);
  print('Numbers x 3 after change: $numbersByThree');
}

Result:

2 * 10
2 * -2
2 * 3
2 * 42
2 * 10
2 * -2
2 * 3
2 * 42
Numbers x 2 after change: (20, -4, 6, 84)
3 * 1
3 * -2
3 * 3
3 * 42
3 * 1
3 * -2
3 * 3
3 * 42
Numbers x 3 before change: (3, -6, 9, 126)
3 * 0
3 * -2
3 * 3
3 * 42
3 * 0
3 * -2
3 * 3
3 * 42
Numbers x 3 after change: (0, -6, 9, 126)

Additional context

No response

I would like to fix this problem.

  • I will try and fix this problem on dart.dev.
@dtonhofer dtonhofer added the from.page-issue Reported in a reader-filed concern label Mar 3, 2024
@atsansone
Copy link
Contributor

@eernstg @munificent : Would you have any insight on this contributor's issue?

@atsansone atsansone added p2-medium Necessary but not urgent concern. Resolve when possible. fix.examples Adds or changes example e1-hours Can complete in < 8 hours of normal, not dedicated, work st.triage.ltw Indicates Lead Tech Writer has triaged a.tut.codelab Relates to codelabs hosted on dart.dev. labels Mar 21, 2024
@atsansone atsansone changed the title [PAGE ISSUE]: 'Iterable collections': Suggesting to add an example showing the "lazy eval" aspects of map() Add an example showing the "lazy eval" aspects of map() to 'Iterable collections' page Mar 21, 2024
@munificent
Copy link
Member

@dtonhofer is correct that Iterable.map() and the other similar methods return lazy collections. That means that the callback you pass isn't executed until the resulting Iterable is actually iterated. And if you iterate it multiple times, it will run the callback multiple times.

Here's a simpler version of their example:

main() {
  final mapped = [1, 2, 3].map((n) {
    print(n);
    return n
  });

  // Iterate the resulting Iterable twice:
  print('Before iterating');
  for (var _ in mapped) {}
  for (var _ in mapped) {}
}

If you run this, it prints:

Before iterating
1
2
3
1
2
3

It can be important for users to understand this if the callback they pass has side effects (like this example) or is computationally expensive. I'm not sure if it's important enough to emphasize it in the docs. It does seem to be a thing that trips users up periodically. The idiomatic workaround is to use a trailing call to .toList() to force eager evaluation when that's what you want.

(The reason these methods are all lazy is so that you can chain a bunch of transformations, call .toList() once at the end, and you only end up allocating a single list, instead of one for each step in the chain.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a.tut.codelab Relates to codelabs hosted on dart.dev. e1-hours Can complete in < 8 hours of normal, not dedicated, work fix.examples Adds or changes example from.page-issue Reported in a reader-filed concern p2-medium Necessary but not urgent concern. Resolve when possible. st.triage.ltw Indicates Lead Tech Writer has triaged
Projects
None yet
Development

No branches or pull requests

3 participants