Chapters

Hide chapters

Dart Apprentice: Beyond the Basics

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Beyond the Basics

Section 1: 15 chapters
Show chapters Hide chapters

2. Anonymous Functions
Written by Jonathan Sande

No, anonymous functions aren’t the secret agents of the Dart world, sneaking around cloak-and-dagger style. They’re just functions without names. In fact, they’re simply values. Just as 2 is an int value, 3.14 is a double value, 'Hello world' is a String value and false is a bool value, an anonymous function is a Function value. You can assign anonymous functions to variables and pass them around as arguments just as you would any other value. Dart treats functions as first-class citizens.

The ability to pass functions around makes it easy to perform an action on every collection element or tell a button to run some code when a user presses it. This chapter will teach you how to do all this and more.

Functions as Values

All the functions you saw in Dart Apprentice: Fundamentals were named functions, which means, well, that they had a name.

String compliment(int number) { ; } return '$number is a very nice number.' Function name
Named function

But not every function needs a name. If you remove the return type and the function name, what’s left is an anonymous function:

String compliment(int number) { ; } return '$number is a very nice number.'
Anonymous function

The return type will be inferred from the return value of the function body — String in this case. Removing the name and return type allows you to treat the resulting anonymous function as a value.

Assigning Functions to Variables

By this point, you’re already familiar with assigning values to variables:

int number = 4;
String greeting = 'hello';
bool isHungry = true;

number is an int, greeting is a String and isHungry is a bool. On the right side of each assignment, you have literal values: 4 is an integer literal, 'hello' is a string literal and true is a Boolean literal.

Assigning a function to a variable works the same way:

Function multiply = (int a, int b) {
  return a * b;
};

multiply is a variable of type Function, and the anonymous function you see to the right of the = equals sign is a function literal.

Passing Functions to Functions

Just as you can write a function to take an int or String value as a parameter, you can also have Function as a parameter:

void namedFunction(Function anonymousFunction) {
  // function body
}

Here, namedFunction takes an anonymous function as a parameter.

Returning Functions From Functions

And just as you can pass in functions as input parameters, you can also return them as output:

Function namedFunction() {
  return () => print('hello');
}

The return value is an anonymous function of type Function. In this case, rather than using curly-brace syntax, you’re using arrow notation.

Higher-Order Functions With Collections

Functions that return functions or accept them as parameters are called higher-order functions. These originally came from functional programming, one of the major programming paradigms, along with object-oriented programming, structural programming and others. Although most people think of Dart as an object-oriented language, it also supports functional programming. You have the flexibility to code in a way that makes sense to you.

One of the most common places you’ll use higher-order functions is with collections. You’ll often want to perform some task on every collection element. Iterable classes in Dart come predefined with many methods that take anonymous functions as parameters.

The image below shows three examples of higher-order functions. Mapping is where you transform every value into a new one. One example would be squaring each value. Filtering allows you to remove elements from a collection, such as by filtering out all the even numbers. Reducing consolidates a collection to a single value, such as by summing the elements.

2 10 12 6 4 8 4 100 144 36 16 64 1 5 6 3 2 1 5 3 4 2 10 12 6 4 8 42 map: filter: reduce:
Common higher-order functions

There are many more methods than this small sample, though. Don’t worry — you’ll discover them in time.

For-Each Loops

while loops and for loops allow you to iterate using an index. for-in loops are convenient for looping over all the elements of a collection without needing an index. Dart collections also have a forEach method that will perform whatever task you like on each collection element.

Iterating Over a List

To see forEach in action, write the following list in main:

const numbers = [1, 2, 3];

Then, call forEach on the list and pass in an anonymous function that triples each number in the list and prints that value:

numbers.forEach((int number) {
  print(3 * number);
});

All those parentheses and curly braces can get a little confusing. To clarify things, here’s the collection with its forEach method:

numbers.forEach(

);

And here’s the anonymous function you’re passing in as an argument:

(int number) {
  print(3 * number);
}

The number is the current element from the list as forEach iterates through the elements. The function body then multiplies that value by three and prints the result.

Run the code, and you’ll see the following in the console:

3
6
9

Because Dart already knows the list elements are of type int, you can omit the type annotation for the function parameter. Replace the expression above with the abbreviated form:

numbers.forEach((number) {
  print(3 * number);
});

This version has no int before number. Dart infers it.

Note: Choosing to omit the type is a matter of preference. The pro is that your code is more concise; the con is that you can’t see at a glance what the type is. Use whatever form you feel is more readable.

Because the anonymous function body only contains a single line, you can replace the curly braces with arrow notation:

numbers.forEach((number) => print(3 * number));

Note that the Effective Dart guide in the Dart documentation recommends against using function literals in forEach loops. The standard way to loop over a collection is with a for-in loop:

for (final number in numbers) {
  print(3 * number);
}

This tends to be easier to read.

If, on the other hand, your function is in a variable, then it’s quite readable to still use a forEach loop:

final triple = (int x) => print(3 * x);
numbers.forEach(triple);

forEach runs triple on every element in numbers.

Iterating Over a Map

Map collections are not iterable, so they don’t directly support for-in loops. However, they do have a forEach method.

Write the following example in main:

final flowerColor = {
  'roses': 'red',
  'violets': 'blue',
};

flowerColor.forEach((flower, color) {
  print('$flower are $color');
});

print('i \u2764 Dart');
print('and so do you');

In this case, the anonymous function has two parameters: flower is the key and color is the value. Because flowerColor is of type Map<String, String>, Dart infers that both flower and color are of type String.

Run your code to read the output:

roses are red
violets are blue
i ❤ Dart
and so do you

You’re a poet and you didn’t know it!

forEach performs a task on each collection element but doesn’t return any values. The higher-order methods that follow will return values.

Mapping One Collection to Another

Say you want to transform all the values of one collection and produce a new collection. One way you could do that is with a loop:

const numbers = [2, 4, 6, 8, 10, 12];

final looped = <int>[];
for (final x in numbers) {
  looped.add(x * x);
}

Print looped to see the squared values:

[4, 16, 36, 64, 100, 144]

Mapping, however, allows you to accomplish the same thing without a loop. Dart collections provide this functionality with a method named map.

Note: This section’s map method differs from the Map data type you’ve studied previously. List, Set and Map all have a map method.

Add the following line of code to main:

final mapped = numbers.map((x) => x * x);

map produces a new collection by taking the anonymous function that you supply and applying it to every element of the existing collection. In the example above, because numbers is a list of int values, x is inferred to be of type int. The first time through the loop, x is 2; the second time through, x is 4; and so on through 12. The anonymous function squares each of these values.

Print mapped to see the result:

(4, 16, 36, 64, 100, 144)

Note the parentheses surrounding the collection elements. They tell you this is an Iterable rather than a List, which would have been printed with square brackets.

If you really want a List instead of an Iterable, call toList on the result:

print(mapped.toList());

Run that, and now you’ll have square brackets:

[4, 16, 36, 64, 100, 144]

It’s a common mistake to forget that map produces an Iterable rather than a List, but now you know what to do. The reason List isn’t the default is for performance sake. Recall that iterables are lazy. The resulting collection from map isn’t computed until you need it.

map gives you a collection with the same number of elements as the original collection. However, the higher-order method in the next section will help you weed out unnecessary elements.

Filtering a Collection

You can filter an iterable collection like List and Set using the where method.

Add the following code to main:

final myList = [1, 2, 3, 4, 5, 6];
final odds = myList.where((element) => element.isOdd);

Like map, the where method takes an anonymous function. The function’s input is also each element of the list, but unlike map, the value the function returns must be a Boolean. This is what happens for each element:

1.isOdd  // true
2.isOdd  // false
3.isOdd  // true
4.isOdd  // false
5.isOdd  // true
6.isOdd  // false

If the function returns true for a particular element, that element is added to the resulting collection, but if it’s false, the element is excluded. Using isOdd makes the condition true for odd numbers, so you’ve filtered down integers to just the odd values.

Print odds, and you’ll get:

(1, 3, 5)

As you can see by the parentheses, where also returns an Iterable.

You can use where with List and Set but not with Map — unless you access the keys or values properties of Map.

Consolidating a Collection

Some higher-order methods take all the elements of an iterable collection and consolidate them into one value using the function you provide. You’ll learn two ways to accomplish this.

Using Reduce

One way to combine all the collection elements into one value is to use the reduce method. You can combine the elements any way you like, but the example below shows how to find their sum.

Given the following list, find the sum of all the elements by passing in an anonymous function that adds each element to the sum of the previous ones:

const evens = [2, 4, 6, 8, 10, 12];
final total = evens.reduce((sum, element) => sum + element);

The first parameter, sum, is the accumulator. It remembers the current total as each element is added. If you were to print sum and element on each function call, this would be what you’d get:

sum: 2,  element: 4
sum: 6,  element: 6
sum: 12, element: 8
sum: 20, element: 10
sum: 30, element: 12

sum starts with the value of the first element in the collection, while element begins with the second element.

Print total to see the final result of 42, which is 2 + 4 + 6 + 8 + 10 + 12.

Try one more example with reduce:

final emptyList = <int>[];
final result = emptyList.reduce((sum, element) => sum + element);

Run this, and you’ll get an error. reduce can’t assign the first element to sum because there’s no first element.

Delete that code and continue reading to see how fold can solve this problem for you.

Using Fold

Because calling reduce on an empty list gives an error, using fold will be more reliable when a collection has a possibility of containing zero elements. The fold method works like reduce, but it takes an extra parameter that provides a starting value for the function.

Here’s the same result as above, but this time using fold:

const evens = [2, 4, 6, 8, 10, 12];
final total = evens.fold(
  0,
  (sum, element) => sum + element,
);

There are two arguments that you gave the fold method. The first argument, 0, is the starting value. The second argument takes that 0, feeds it to sum and keeps adding to it based on the value of each element in the list.

If you were to check the values of sum and element on each iteration, you’d get the following:

sum: 0,  element: 2
sum: 2,  element: 4
sum: 6,  element: 6
sum: 12, element: 8
sum: 20, element: 10
sum: 30, element: 12

This time, you can see that on the first iteration, sum is initialized with 0 while element is the first element in the collection.

Print total again to see that the final result is still 42, as it was with reduce.

Now, try the empty list example using fold:

final emptyList = <int>[];
final result = emptyList.fold(
  0,
  (sum, element) => sum + element,
);
print(result);

Run that, and you’ll get 0 — no crash with fold.

Sorting a List

You’ve previously learned how to sort a list. For a refresher, though, call sort on the desserts list below:

final desserts = ['cookies', 'pie', 'donuts', 'brownies'];
desserts.sort();

Print desserts, and you’ll see the following:

[brownies, cookies, donuts, pie]

sort put them in alphabetical order. This is the default sorting order for strings.

Dart also allows you to define other sorting orders. The way to accomplish that is to pass in an anonymous function as an argument to sort. Say you want to sort strings by length and not alphabetically. Give sort an anonymous function like so:

desserts.sort((d1, d2) => d1.length.compareTo(d2.length));

The names d1 and d2 aren’t going to win any good naming prizes, but they fit on the page of a book better than dessertOne and dessertTwo do.

The compareTo method returns three possible values:

  • -1 if the first value is smaller.
  • 1 if the first value is larger.
  • 0 if both values are the same.

The values you’re comparing here are the string lengths. This is all that sort needs to perform the custom sort.

Print desserts again, and you’ll see the list is sorted by the length of each string:

[pie, donuts, cookies, brownies]

Combining Higher-Order Methods

You can chain higher-order methods together. For example, if you wanted to take only the desserts that have a name length greater than 5 and then convert those names to uppercase, you’d do it like so:

const desserts = ['cake', 'pie', 'donuts', 'brownies'];
final bigTallDesserts = desserts
    .where((dessert) => dessert.length > 5)
    .map((dessert) => dessert.toUpperCase())
    .toList();

First, you filtered the list with where, then you mapped the remaining elements to uppercase strings and finally converted the iterable to a list.

Printing bigTallDesserts reveals:

[DONUTS, BROWNIES]

Using chains of higher-order methods like this is called declarative programming and is one of the common features of functional programming. Previously, you’ve mostly used imperative programming, in which you tell the computer exactly how to calculate the result you want. With declarative programming, you describe the result you want and let the computer determine the best way to get there.

Here’s how you would get the same result as you did using the code above, but imperatively:

const desserts = ['cake', 'pie', 'donuts', 'brownies'];
final bigTallDesserts = <String>[];
for (final item in desserts) {
  if (item.length > 5) {
    final upperCase = item.toUpperCase();
    bigTallDesserts.add(upperCase);
  }
}

That’s not quite as readable, is it?

Exercise

Given the following exam scores:

final scores = [89, 77, 46, 93, 82, 67, 32, 88];
  1. Use sort to order the grades from highest to lowest.
  2. Use where to find all the B grades, that is, all the scores between 80 and 90.

Callback Functions

When writing an application, you often want to run some code to handle an event, whether that event is a user pressing a button or an audio player reaching the end of the song. The functions that handle these events are called callback functions. They’re another important use of anonymous functions.

You don’t have to do much Flutter programming before you meet a callback function. For example, here’s how you might create a TextButton in Flutter:

TextButton(
  child: Text('Click me!'),
  onPressed: () {
    print('Clicked');
  },
)

TextButton is the class name, and it has two required named parameters: child and onPressed. The item of interest here is onPressed, which takes an anonymous function as the callback. Flutter runs the code in the callback function whenever the button is pressed.

In the example here, you simply print “Clicked”. But the beauty of letting the user supply the callback is that your button can do anything. It could send a message, turn on the TV or launch a nuclear missile. Please don’t use it for the latter, though.

Void Callback

The example below will walk you through building a button with a callback method. Because the anonymous function doesn’t take any parameters or return a value, it’s commonly referred to as a void callback.

Implementing a Class That Takes a Void Callback

Write the following class outside of main:

class Button {
  Button({
    required this.title,
    required this.onPressed,
  });

  final String title;
  final Function onPressed;
}

onPressed is a field name that will store whatever anonymous function the user passes in.

Create an instance of your Button in main like so:

final myButton = Button(
  title: 'Click me!',
  onPressed: () {
    print('Clicked');
  },
);

If you were building a full-fledged Button widget, you’d probably call onPressed from somewhere within your class. However, because you haven’t implemented that for such a basic example, you can just call the function externally as a proof of concept. Add the following line at the bottom of main:

myButton.onPressed();

The name onPressed without parentheses is the function itself, whereas onPressed() with parentheses calls the function. An alternative way to execute the function code is by calling the call method on the function:

myButton.onPressed.call();

Run your code to check that “Clicked” prints to the console.

Specifying the Function Type

The example above works well, but there’s one minor problem.

Create another button like so:

final anotherButton = Button(
  title: 'Click me, too!',
  onPressed: (int apple) {
    print('Clicked');
    return 42;
  },
);

In this case, you passed in an anonymous function that has a parameter named apple and returns the integer 42. Where does that apple come from? Where does that 42 go? Nowhere. It isn’t the void function that your implementation is expecting. If you run that function, you get a runtime crash.

A better approach would be to let users of your Button know at compile time that they can only give onPressed a void function.

To do that, find your Button class and replace the line final Function onPressed; with the following:

final void Function() onPressed;

The void ensures users can’t supply a return value, and the () empty parentheses ensure that they can’t give you a function with parameters.

The compiler lets you know that it doesn’t like anotherButton, so delete that from main.

Value Setter Callback

Suppose you wanted to allow the user to run some code every time an update came from within the widget. An example of this is an audio seek bar that notifies about the thumb’s horizontal position while a user drags it.

Add the following class outside of main:

class MyWidget {
  MyWidget({
    required this.onTouch,
  });

  final void Function(double xPosition) onTouch;
}

MyWidget stores a function that requires an argument when it’s called.

Create an instance of MyWidget with its callback method in main like so:

final myWidget = MyWidget(
  onTouch: (x) => print(x),
);

Whenever onTouch is executed, this function says to print the value of the x position.

Normally, you would call onTouch internally within the widget as you listen to a gesture detector, but you can call onTouch externally as well. Write the following in main:

myWidget.onTouch(3.14);

Because the function caller sets the parameter value, this is a value setter callback.

Value Getter Callback

Sometimes your class needs to ask for a value dynamically. In that case, you need a value getter callback, which is an anonymous function that returns a value.

Add the following class outside of main:

class AnotherWidget {
  AnotherWidget({
    this.timeStamp,
  });

  final String Function()? timeStamp;
}

In this case, the callback function is nullable, making it optional.

Create a new instance of the widget in main:

final myWidget = AnotherWidget(
  timeStamp: () => DateTime.now().toIso8601String(),
);

Setting the timeStamp property allows your widget to call the function anytime to retrieve the value. An ISO-8601 string is a convenient format when you need to store a time stamp.

As with previous examples, timeStamp is normally a function that your widget would call internally, but you can also call it externally:

final timeStamp = myWidget.timeStamp?.call();
print(timeStamp);

In this case, you can’t call the function as timeStamp() with parentheses because the function will be null if the user didn’t provide one. However, you can use the ?. null-aware method invocation operator to optionally execute the function using call().

Run the code above to see the time stamp:

2022-10-12T14:59:14.438099

That’s the precise time this chapter was being prepared for publishing.

Simplifying With Tear-Offs

When you have a function, you can either execute the function immediately or hold a reference to it. The difference is in the parentheses:

  • myFunction(): executes the function code immediately.
  • myFunction: references the function without executing the code.

Being able to reference a function by its name allows you to make some simplifications.

For example, add the following class outside of your main method:

class StateManager {
  int _counter = 0;

  void handleButtonClick() {
    _counter++;
  }
}

This class represents a simple state management class that you might use in Flutter.

Now, replace the body of main with the following content:

final manager = StateManager();

final myButton = Button(
  title: 'Click me!',
  onPressed: () {
    manager.handleButtonClick();
  },
);

Pay attention to the anonymous function that you passed to onPressed. You’ll see many people writing code like this. The author does it all the time. You can do better, though.

The () parentheses at the end of handleButtonClick() tell Dart to execute this function, but the () { } syntax for the anonymous function tells Dart not to execute this function yet. Dart stores it in the onPressed property for possible execution later. You’ve got a command to execute and a command to not execute. These cancel each other out, so you have an opportunity to simplify that syntax.

Find these three lines:

onPressed: () {
  manager.handleButtonClick();
},

And replace them with this line:

onPressed: manager.handleButtonClick,

Because handleButtonClick doesn’t have parentheses after it, it isn’t executed right away. This is known as a tear-off. You tore the handleButtonClick method off and converted it to a function object to be stored in the onPressed property. This syntax is much cleaner.

Tear-offs work in other places, too. Say you want to print each element in a list. You could do that like so:

const cities = ['Istanbul', 'Ankara', 'Izmir', 'Bursa', 'Antalya'];
cities.forEach((city) => print(city));

But because the anonymous function and print have the same property, city, you can use a tear-off instead:

cities.forEach(print);

Run that to see the names of each of these large Turkish cities printed to the console:

Istanbul
Ankara
Izmir
Bursa
Antalya

Renaming With Type Aliases

One more way to simplify your syntax is by using typedef, which is short for “type definition”. This keyword allows you to give an alias to a long type name so that it’s shorter and easier to understand.

Take this example:

class Gizmo {
  Gizmo({
    required this.builder,
  });

  final Map<String, int> Function(List<int>) builder;
}

The type Map<String, int> Function(List<int>) is a function that takes a list of integers as input and returns a map of String-to-int key-values pairs. That’s quite complex and hard to read.

Add a type alias for the function outside of Gizmo:

typedef MapBuilder = Map<String, int> Function(List<int>);

MapBuilder is the alias for your complex function signature.

Now, you can rewrite your Gizmo class like so:

class Gizmo {
  Gizmo({
    required this.builder,
  });

  final MapBuilder builder;
}

This is much more readable. Flutter takes this approach of giving aliases for many of its callback and builder functions.

You can use typedef to rename other types as well. For example, write the following line outside of main:

typedef ZipCode = int;

This doesn’t create a new type. Instead, ZipCode is just another way of referring to the int type. You can observe that in the code below:

Write the following in main:

ZipCode code = 87101;
int number = 42;

print(code is ZipCode);   // true
print(code is int);       // true
print(number is ZipCode); // true
print(number is int);     // true

The purpose of the is keyword is to distinguish between types. However, in this case, is treats int and its alias ZipCode exactly the same … because they’re the same.

Note: If you need a new type to store postal codes, you should create a class and not a type alias. This will allow you to distinguish the postal code type from int and validate its data. For example, you probably wouldn’t want to allow numbers like -1 or 42 to be postal codes.

Exercise

  1. Create a class named Surface.
  2. Give the class a property named onTouch, a callback function that provides x and y coordinates but returns nothing.
  3. Make a type alias named TouchHandler for the callback function.
  4. In Surface, create a method named touch, which takes x and y coordinates and then internally calls onTouch.
  5. In main, create an instance of Surface and pass in an anonymous function that prints the x and y coordinates.
  6. Still in main, call touch where x is 202.3 and y is 134.0.

Closures and Scope

Anonymous functions in Dart act as closures. The term closure means that the code “closes around” the surrounding scope and therefore has access to variables and functions defined within that scope.

{ age = ; (parameters) { age = ; } } a scope var another scope // have access to age 42 43
Scope

A scope in Dart is defined by a pair of curly braces. All the code within these braces is a scope. You can even have nested scopes within other scopes. Examples of scopes are function bodies and the bodies of loops.

Closure Example

Write the following in main:

var counter = 0;
final incrementCounter = () {
  counter += 1;
};

The anonymous function that defines incrementCounter acts as a closure. It can access counter, even though counter is neither a parameter of the anonymous function nor defined in the function body.

Call incrementCounter five times and print counter:

incrementCounter();
incrementCounter();
incrementCounter();
incrementCounter();
incrementCounter();
print(counter); // 5

You’ll see that counter now has a value of 5.

A Function That Counts Itself

If you return a closure from a function, that function will be able to count the number of times it was called. To see this in action, add the following function outside of main:

Function countingFunction() {
  var counter = 0;
  final incrementCounter = () {
    counter += 1;
    return counter;
  };
  return incrementCounter;
}

Each function returned by countingFunction will have its own version of counter. So if you were to generate two functions with countingFunction, like so:

final counter1 = countingFunction();
final counter2 = countingFunction();

…then each call to those functions will increment its own counter independently:

print(counter1()); // 1
print(counter2()); // 1
print(counter1()); // 2
print(counter1()); // 3
print(counter2()); // 2

Admittedly, you probably won’t write self-counting functions every day. But this example demonstrated another aspect of the Dart programming language.

In this chapter, you learned a bit about functional programming. In the next chapter, you’ll dive into the essentials of object-oriented programming.

Challenges

Before moving on, here are some challenges to test your knowledge of anonymous functions. It’s best if you try to solve them yourself, but solutions are available with the supplementary materials for this book if you get stuck.

Challenge 1: Animalsss

Given the map below:

final animals = {
  'sheep': 99,
  'goats': 32,
  'snakes': 7,
  'lions': 80,
  'seals': 18,
};

Use higher-order functions to find the total number of animals whose names begin with “s”. How many sheep, snakes and seals are there?

Challenge 2: Can You Repeat That?

Write a function named repeatTask with the following definition:

int repeatTask(int times, int input, Function task)

It repeats a given task on input for times number of times.

Pass an anonymous function to repeatTask to square the input of 2 four times. Confirm that you get the result 65536 because 2 squared is 4, 4 squared is 16, 16 squared is 256 and 256 squared is 65536.

Key Points

  • Anonymous functions don’t have a function name, and the return type is inferred.
  • Dart functions are first-class citizens and thus can be assigned to variables and passed around as values.
  • Dart supports both functional and object-oriented programming paradigms.
  • Higher-order functions are functions that return functions or accept them as parameters.
  • Dart collections contain many methods that accept anonymous functions as parameters. Examples include forEach, map, where, reduce and fold.
  • Chaining higher-order methods together is typical of declarative programming and allows you to solve many problems without the loops of imperative programming.
  • Callback functions are anonymous functions that you provide to handle events.
  • Tear-offs are function objects with the same parameters as the method you pass them to, which allows you to omit the parameters altogether.
  • The typedef keyword allows you to rename types so they’re shorter or easier to understand.
  • Anonymous functions act as closures, capturing any variables or functions within their scope.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.