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.
But not every function needs a name. If you remove the return type and the function name, what’s left is an 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.
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 theMap
data type you’ve studied previously.List
,Set
andMap
all have amap
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];
- Use
sort
to order the grades from highest to lowest. - Use
where
to find all the B grades, that is, all the scores between80
and90
.
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
or42
to be postal codes.
Exercise
- Create a class named
Surface
. - Give the class a property named
onTouch
, a callback function that provides x and y coordinates but returns nothing. - Make a type alias named
TouchHandler
for the callback function. - In
Surface
, create a method namedtouch
, which takes x and y coordinates and then internally callsonTouch
. - In
main
, create an instance ofSurface
and pass in an anonymous function that prints the x and y coordinates. - Still in
main
, calltouch
where x is202.3
and y is134.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.
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
andfold
. - 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.