7 Functions¶
Each week, there are tasks that you repeat over and over: eat breakfast, brush your teeth, write your name, read books about Dart, and so on. Each of those tasks can be divided up into smaller tasks. Brushing your teeth, for example, includes putting toothpaste on the brush, brushing each tooth and rinsing your mouth out with water.
The same idea exists in computer programming. A function is one small task, or sometimes a collection of several related tasks, that you can use in conjunction with other functions to accomplish a larger task. In this chapter, you’ll learn how to write functions in Dart.
Function Basics¶
You can think of functions like machines. They take something you provide to them, the input, and produce something different, the output.
There are many examples of this in daily life. With an apple juicer, you put in apples and you get out apple juice. The input is apples; the output is juice. A dishwasher is another example. The input is dirty dishes, and the output is clean dishes. Blenders, coffee makers, microwaves and ovens are all like real-world functions that accept an input and produce an output.
Don’t Repeat Yourself¶
Assume you have a small, useful piece of code that you’ve repeated in multiple places throughout your program:
// one place
if (fruit == 'banana') {
peelBanana();
eatBanana();
}
// another place
if (fruit == 'banana') {
peelBanana();
eatBanana();
}
// some other place
if (fruit == 'banana') {
peelBanana();
eatBanana();
}
Now, that code works rather well, but repeating that code in multiple spots presents at least two problems. The first problem is that you’re duplicating effort by having this code in multiple places in your program. The second and more troubling problem is that if you need to change the logic in that bit of code later on, you’ll have to track down all of those instances of the code and change them in the same way. It’s likely that you’ll make a mistake somewhere, or even miss changing one of the instances because you didn’t see it.
Over time, this problem has led to some sound advice for writing clean code: don’t repeat yourself, abbreviated as DRY. This term was originally coined in the book The Pragmatic Programmer by Andrew Hunt and David Thomas. Writing DRY code will prevent many bugs from creeping into your programs.
Functions are one of the main solutions to the duplication problem in the example above. Instead of repeating blocks of code in multiple places, you can simply package that code into a function and call that function from wherever you need to.
Anatomy of a Dart Function¶
In Dart, a function consists of a return type, a name, a parameter list in parentheses and a body enclosed in braces.
Here is a summary of the labeled parts of the function:
- Return type: This comes first. It tells you immediately what the type will be of the function output. This particular function will return a
String
, but your functions can return any type you like. If the function won’t return anything, that is, if it performs some work but doesn’t produce an output value, you can usevoid
as the return type. - Function name: You can name functions almost anything you like, but you should follow the
lowerCamelCase
naming convention. You’ll learn a few more naming conventions a little later in this chapter. - Parameters: Parameters are the input to the function. They go inside the parentheses after the function name. This example has only one parameter, but if you had more than one, you would separate them with commas. For each parameter, you write the type first, followed by the name. Just as with variable names, you should use
lowerCamelCase
for your parameter names. - Return value: This is the function’s output, and it should match the return type. In the example above, the function returns a
String
value by using thereturn
keyword. If the return type isvoid
, though, then you don’t return anything.
The return type, function name and parameters are collectively known as the function signature:
String compliment(int number)
The code between the braces is known as the function body:
{
return '$number is a very nice number.';
}
This is what the function above looks like in the context of a program:
void main() {
const input = 12;
final output = compliment(input);
print(output);
}
String compliment(int number) {
return '$number is a very nice number.';
}
What have we here? Not one function, but two? Yes, main
is also a function, and one you’ve seen many times already. It’s the function that every Dart program starts with. Since main
doesn’t return a value, the return type of main
must be void
. Although main
can take parameters, there aren’t any in this case, so there’s only a pair of empty parentheses that follow the function name.
Notice that the compliment
function is outside of main
. Dart supports top-level functions, which are functions that aren’t inside a class or another function. Conversely, you may nest one function inside another. And when a function is inside a class, it’s called a method, which you’ll learn more about in Chapter 8, “Classes”.
You call a function by writing its name and providing the argument, which is the value you provide inside the parentheses as the parameter to the function. In this case, you’re calling the compliment
function and passing in an argument of 12
. Run the code now and you’ll see the following result:
12 is a very nice number.
Indeed, twelve is a nice number. It’s the largest one-syllable number in English.
Note
It’s easy to get the words parameter and argument mixed up. A parameter is the name and type that you define as an input for your function. An argument, on the other hand, is the actual value that you pass in. A parameter is abstract, while an argument is concrete.
Parameters¶
Parameters are incredibly flexible in Dart, so they deserve their own section.
Using Multiple Parameters¶
In a Dart function, you can use any number of parameters. If you have more than one parameter for your function, simply separate them with commas. Here’s a function with two parameters:
void helloPersonAndPet(String person, String pet) {
print('Hello, $person, and your furry friend, $pet!');
}
Parameters like the ones above are called positional parameters because you have to supply the arguments in the same order that you defined the parameters when you wrote the function. If you call the function with the parameters in the wrong order, you’ll get something obviously wrong:
helloPersonAndPet('Fluffy', 'Chris');
// Hello, Fluffy, and your furry friend, Chris!
Making Parameters Optional¶
The function above was very nice, but it was a little rigid. For example, try the following:
helloPersonAndPet();
If you don’t have exactly the right number of parameters, the compiler will complain to you:
2 positional argument(s) expected, but 0 found.
You defined helloPersonAndPet
to take two arguments, but in this case, you didn’t pass in any. It would be nice if the code could detect this, and just say, “Hello, you two!” if no names are provided. Thankfully, it’s possible to have optional parameters in a Dart function.
Imagine you want a function that takes a first name, a last name and a title, and returns a single string with the various pieces of the person’s name strung together:
String fullName(String first, String last, String title) {
return '$title $first $last';
}
The thing is, not everyone has a title, or wants to use their title, so your function needs to treat the title as optional. To indicate that a parameter is optional, you surround the parameter with square brackets and add a question mark after the type, like so:
String fullName(String first, String last, [String? title]) {
if (title != null) {
return '$title $first $last';
} else {
return '$first $last';
}
}
Here are a couple of points to note about the code above:
- Putting square brackets around
String? title
makestitle
optional. The?
afterString
means it’s a nullable type. If you don’t pass in a value fortitle
, then it will have the value ofnull
, which means “no value”. The updated code checks fornull
to decide how to format the return string. - Optional parameters must go at the end of the parameter list. For example, you couldn’t put
[String? title]
beforefirst
orlast
in the example above. The only exception to that is if you were to make all of the parameters optional, in which case you would surround the entire parameter list with the square brackets.
Write these two examples to test your code out:
print(fullName('Ray', 'Wenderlich'));
print(fullName('Albert', 'Einstein', 'Professor'));
Run that now and you’ll see the following:
Ray Wenderlich
Professor Albert Einstein
The function correctly handles the optional title.
Note
Technically speaking, the question mark in String?
is not written after the type; it’s an integral part of the type, that is, the nullable String?
type. More on this in Chapter 11, “Nullability”.
Providing Default Values¶
In the example above, you saw that the default value for an optional parameter was null
. This isn’t always the best value for a default, though. That’s why Dart also gives you the power to change the default value of any parameter in your function by using the assignment operator.
Take a look at this example:
bool withinTolerance(int value, [int min = 0, int max = 10]) {
return min <= value && value <= max;
}
There are three parameters here, two of which are optional: min
and max
. If you don’t specify a value for them, then min
will be 0
and max
will be 10
.
Here are some specific examples to illustrate that:
withinTolerance(5) // true
withinTolerance(15) // false
Since 5
is between 0
and 10
, this evaluates to true
; but since 15
is greater than the default max of 10
, it evaluates to false
.
If you want to specify values other than the defaults, you can do that as well:
withinTolerance(9, 7, 11) // true
Since 9
is between 7
and 11
, the function returns true
.
Look at that function call again: withinTolerance(9, 7, 11)
. Imagine that you’re reading through your code for the first time in a month. What do those three numbers even mean? If you’ve got a good memory, you might recall that one of them is value
, but which one? The first one? Or was it the second one? Or maybe it was the last one.
If that wasn’t bad enough, the following function call also returns true
:
withinTolerance(9, 7) // true
Since the function uses positional parameters, the provided arguments must follow the order you defined the parameters. That means value
is 9
, min
is 7
and max
has the default of 10
. But who could ever remember that?
Of course, you could just Command+click the function name on a Mac, or Control+clickon a PC, to go to the definition and remind yourself of what the parameters meant. But the point is that this code is extremely hard to read. If only there were a better way!
Well, now that you mention it…
Naming Parameters¶
Dart allows you to use named parameters to make the meaning of the parameters more clear in function calls.
To create a named parameter, surround it with curly braces instead of square brackets. Here’s the same function as above, but using named parameters instead:
bool withinTolerance(int value, {int min = 0, int max = 10}) {
return min <= value && value <= max;
}
Note the following:
min
andmax
are surrounded by braces, which means you must use the parameter names when you provide their argument values to the function.- Like square brackets, curly braces make the parameters inside optional. Since
value
isn’t inside the braces, though, it’s still required.
To provide an argument, you use the parameter name, followed by a colon and then the argument value. Here is how you call the function now:
withinTolerance(9, min: 7, max: 11) // true
That’s a lot clearer, isn’t it? The names min
and max
make it obvious where the tolerance limits are now.
An additional benefit of named parameters is that you don’t have to use them in the exact order in which they were defined. These are both equivalent ways to call the function:
withinTolerance(9, min: 7, max: 11) // true
withinTolerance(9, max: 11, min: 7) // true
They can also go before or after the positional parameter:
withinTolerance(min: 7, max: 11, 9) // true
withinTolerance(min: 7, 9, max: 11) // true
And since named parameters are optional, that means the following function calls are also valid:
withinTolerance(5) // true
withinTolerance(15) // false
withinTolerance(5, min: 7) // false
withinTolerance(15, max: 20) // true
In the first two lines, since min
is 0
and max
is 10
by default, values of 5
and 15
evaluate to true
and false
respectively. In the last two lines, the min
and max
defaults were changed, which also changed the outcomes of the evaluations.
Making Named Parameters Required¶
You might like to make value
a named parameter as well. That way you could call the function like so:
withinTolerance(value: 9, min: 7, max: 11)
However, this brings up a problem. Named parameters are optional by default, but value
can’t be optional. If it were, someone might try to use your function like this:
withinTolerance()
Should that return true
or false
? It doesn’t make sense to return anything if you don’t give the function a value. This is just a bug waiting to happen.
What you want is to make value
required instead of optional, while still keeping it as a named parameter. You can achieve this by including value
inside the curly braces and adding the required
keyword in front:
bool withinTolerance({
required int value,
int min = 0,
int max = 10,
}) {
return min <= value && value <= max;
}
Since the function signature was getting a little long, adding a comma after the last parameter lets the IDE format it vertically. You still remember how to auto-format in VS Code, right? That’s Shift+Option+F on a Mac or Shift+Alt+F on a PC.
With the required
keyword in place, VS Code will warn you if you don’t provide a value for value
when you call the function:
Using named parameters makes your code more readable and is an important part of writing clean code when you have multiple inputs to a function. In the next section, you’ll learn some more best practices for writing good functions.
Exercises¶
- Write a function named
youAreWonderful
, with a string parameter calledname
. It should return a string usingname
, and say something like “You’re wonderful, Bob.” - Add another
int
parameter to that function callednumberPeople
so that the function returns something like “You’re wonderful, Bob. 10 people think so.” - Make both inputs named parameters. Make
name
required and setnumberPeople
to have a default of30
.
Writing Good Functions¶
People have been writing code for decades. Along the way, they’ve come up with some good practices to improve code quality and prevent errors. One of those practices is writing DRY code as you saw earlier. Here are a few more things to pay attention to as you learn about writing good functions.
Avoiding Side Effects¶
When you take medicine to cure a medical problem, but that medicine gives you a headache, that’s known as a side effect. If you put some bread in a toaster to make toast, but the toaster burns your house down, that’s also a side effect. Not all side effects are bad, though. If you take a business trip to Paris, you also get to see the Eiffel Tower. Magnifique!
When you write a function, you know what the inputs are: the parameters. You also know what the output is: the return value. Anything beyond that, that is, anything that affects the world outside of the function, is a side effect.
Have a look at this function:
void hello() {
print('Hello!');
}
Printing something to the console is a side effect because it’s affecting the world outside of the function. If you wanted to rewrite your function so that there were no side effects, you could write it like this:
String hello() {
return 'Hello!';
}
Now, there’s nothing inside the function body that affects the outside world. You’ll have to write the string to the console somewhere outside of the function.
It’s fine, and even necessary, for some functions to have side effects. But as a general rule, functions without side effects are easier to deal with and reason about. You can rely on them to do exactly what you expect because they always return the same output for any given input. These kinds of functions are also called pure functions.
Here is another function with side effects to further illustrate the point:
var myPreciousData = 5782;
String anInnocentLookingFunction(String name) {
myPreciousData = -1;
return 'Hello, $name. Heh, heh, heh.';
}
Unless you took the time to study the code inside of anInnocentLookingFunction
, you’d have no idea that calling this innocent-looking function would also change your precious data. That’s because the function had an unknown side effect. This is also a good reminder about the dangers of using global variables like myPreciousData
. You never know who might change it.
Make it your ambition to maximize your use of pure functions and minimize your use of functions with side effects.
Doing Only One Thing¶
Proponents of “clean code” recommend keeping your functions small and logically coherent. Small here means only a handful of lines of code. If a function is too big, or contains unrelated parts, consider breaking it into smaller functions.
Write your functions so that each one has only a single job to do. If you find yourself adding comments to describe different sections of a complex function, that’s usually a good clue that you should break your function up into smaller functions. In clean coding, this is known as the Single Responsibility Principle. In addition to functions, this principle also applies to classes and libraries. But that’s a topic for another chapter.
Choosing Good Names¶
You should always give your functions names that describe exactly what they do. If your code sounds like well-written prose, it’ll be faster to read and easier to understand.
This naming advice applies to almost every programming language. However, there are a few additional naming conventions that Dart programmers like to follow. These are recommendations, not requirements, but keep them in mind as you code:
- Use noun phrases for pure functions; that is, ones without side effects. For example, use
averageTemperature
instead ofgetAverageTemperature
andstudentNames
instead ofextractStudentNames
. - Use verb phrases for functions with side effects. For example,
updateDatabase
orprintHello
. - Also use verb phases if you want to emphasize that the function does a lot of work. For example,
calculateFibonacci
orparseJson
. - Don’t repeat parameter names in the function name. For example, use
cube(int number)
instead ofcubeNumber(int number)
, orprintStudent(String name)
instead ofprintStudentName(String name)
.
To be clear regarding the last point, it’s not wrong to repeat the parameter name in the function name. However, there are a few reasons to prefer not repeating:
- All other things being equal, short names are easier to read than long names. Long function names are good when they’re descriptive, but redundant information doesn’t convey additional information. Since Dart supports named parameters, it’s easy to make this information visible to the reader. Languages like C, C++ and Java don’t have named parameters so it might be better to include the parameter name in the function name when programming in these languages.
- Longer function names make it more likely that the
dart format
tool will have to wrap a given line of code. Code that line-wraps is often harder to read than code that appears on a single line.
As a general rule, choose the function name that’s the most readable.
Optional Types¶
Earlier you saw this function:
String compliment(int number) {
return '$number is a very nice number.';
}
The return type is String
, and the parameter type is int
. Dart is an optionally-typed language, so it’s possible to omit the types from your function declaration. In that case, the function would look like this:
compliment(number) {
return '$number is a very nice number.';
}
Dart can infer that the return type here is String
, but it has to fall back on dynamic
for the unknown parameter type. The following function is the equivalent of what Dart sees:
String compliment(dynamic number) {
return '$number is a very nice number.';
}
While it’s permissible to omit return and parameter types, this book recommends that you include them at the very least for situations where Dart can’t infer the type. As you learned in Chapter 3, “Types & Operations”, there’s a much greater advantage to writing Dart in a statically-typed way.
Arrow Functions¶
Dart has a special syntax for functions whose body is only one line. Consider the following function named add
that adds two numbers together:
int add(int a, int b) {
return a + b;
}
Since the body is only one line, you can convert it to the following form:
int add(int a, int b) => a + b;
You replaced the function’s braces and body with an arrow (=>
) and left off the return
keyword. The return value is whatever the value of the expression is. Writing a function in this way is known as arrow syntax or arrow notation.
You can’t use arrow syntax if the function body has more than one line, though, as in the following example:
void printTripled(int number) {
final tripled = number * 3;
print(tripled);
}
However, if you rewrote the body to fit on one line, arrow syntax would work:
void printTripled(int number) => print(number * 3);
You should now have a basic understanding of Dart functions. The functions you studied in this chapter were all named functions. In Dart Apprentice: Beyond the Basics, you’ll learn about anonymous functions, which will take your skills to a whole new level.
Challenges¶
Before moving on, here are some challenges to test your knowledge of functions. It’s best if you try to solve them yourself, but solutions are available in the challenge folder for this chapter if you get stuck.
Challenge 1: Circular Area¶
Write a function that tells you the area of a circle based on some input radius. If you recall from geometry class, you can find the area of a circle by multiplying pi times the radius squared.
Challenge 2: Prime Time¶
Write a function that checks if a number is prime.
First, write a function with the following signature to determine if one number is divisible by another:
bool isNumberDivisible(int number, int divisor)
The modulo operator %
will help with that.
Then, write the function that returns true
if prime and false
otherwise:
bool isPrime(int number)
A number is prime if it’s only divisible by 1 and itself. Loop through the numbers from 1 to the number and find the number’s divisors. If it has any divisors other than 1 and itself, it’s not prime.
Check the following cases:
isPrime(6); // false
isPrime(13); // true
isPrime(8893); // true
Here are a few more hints:
- Numbers less than zero are not considered prime.
- Use a
for
loop to look for divisors. You can start at 2 and if you end before the number, return false. - If you’re clever, you can loop from 2 until you reach the square root of the number. Add the following import to the top of the file to access the
sqrt
function:
import 'dart:math';
Key Points¶
- Functions package related blocks of code into reusable units.
- A function signature includes the return type, name and parameters. The function body is the code between the braces.
- Parameters can be positional or named, and required or optional.
- Side effects are anything besides the return value that change the world outside of the function body.
- To write clean code, use functions that are short and only do one thing.
Where to Go From Here?¶
This chapter spoke briefly about the Single Responsibility Principle and other clean coding principles. Do a search for SOLID principles to learn even more. It’ll be time well spent.
Some aspects of Dart are hard-and-fast rules. Other aspects, like how to name variables and functions, are common practices, and while not required, they do make it easier to read the code and share it with others. Improve your understanding of these conventions by reading Effective Dart in the dart.dev guides.