DMarlon Studio

Lesson 19 · 19 of 20

Iterables and generators · Dart

Produce lazy sequences in Dart with sync*, yield and yield*: work with infinite series and compute only what is actually requested.

Dart · Iterables and generators in DMarlon Studio: the example's code and its output in the terminal

A **sync* function returns a lazy `Iterable`: its body does not run when you call it but as values are requested, and each yield** hands over one value and pauses the function until the next request. invoiceNumbers is an endless loop, and that is fine: skip(41).take(3) asks for exactly numbers 42 to 44 and the function goes no further.

readPrices prints a line every time it hands over a price, so you can see when it works. The chain readPrices().where(...).map(...) reads nothing yet: it only describes the query, which is why Query built, nothing read yet appears before any reading. Iterable methods such as where, map, skip and take are lazy too.

Asking for expensive.first starts the work: 120, 45 and 990 are read, and on reaching the first one above 500 the VAT is added with integer arithmetic (* 121 ~/ 100) and everything stops. The prices 300 and 1500 are never read, which matters with thousands of records or an expensive data source. Beware: iterating over expensive twice runs the function twice; store the result with toList() if you are going to reuse it.

yield*** hands over every value of another iterable. chunks uses it to be recursive: it yields the first block of size items and delegates the rest to another call to itself. It is a handy way to paginate a list.

Code · main.dart

// sync* returns a lazy Iterable: the body runs only as values are requested.Iterable<String> invoiceNumbers(String prefix) sync* {  for (var n = 1;; n++) {    yield '$prefix-${n.toString().padLeft(4, '0')}'; // infinite, and that's fine  }}Iterable<int> readPrices() sync* {  for (final price in [120, 45, 990, 300, 1500]) {    print('  reading $price');    yield price;  }}// yield* hands over every value of another iterable.Iterable<List<T>> chunks<T>(List<T> items, int size) sync* {  if (items.length <= size) {    yield items;    return;  }  yield items.sublist(0, size);  yield* chunks(items.sublist(size), size);}void main() {  print(invoiceNumbers('INV').skip(41).take(3).toList());  final expensive = readPrices().where((p) => p > 500).map((p) => p * 121 ~/ 100);  print('Query built, nothing read yet');  print('First expensive with VAT: ${expensive.first}'); // stops at 990  print('Pages: ${chunks(['ana', 'luis', 'eva', 'tom', 'iker'], 2).toList()}');}

Output · dart run main.dart

[INV-0042, INV-0043, INV-0044]
Query built, nothing read yet
  reading 120
  reading 45
  reading 990
First expensive with VAT: 1197
Pages: [[ana, luis], [eva, tom], [iker]]

Key points

  • sync* returns an Iterable that is computed as you iterate.
  • yield hands over one value; yield*, all the values of another iterable.
  • A generator can be infinite as long as you only ask for a few values.
  • where, map, skip and take do nothing until the result is iterated.
  • Each iteration reruns the generator; use toList() to keep the result.

Exercise

Write an infinite fibonacci() generator with sync* and print the first Fibonacci number greater than 1000 using firstWhere.