Home Flutter Books Dart Apprentice

5
Functions Written by Jonathan Sande

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 smaller, 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, including both named functions and anonymous functions.

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();
}

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.

void main() {
  const input = 12;
  final output = compliment(input);
  print(output);
}

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

More about 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!');
}
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();
2 positional argument(s) expected, but 0 found.
String fullName(String first, String last, String title) {
  return '$title $first $last';
}
String fullName(String first, String last, [String title]) {
  if (title != null) {
    return '$title $first $last';
  } else {
    return '$first $last';
  }
}
print(fullName('Ray', 'Wenderlich'));
print(fullName('Albert', 'Einstein', 'Professor'));
Ray Wenderlich
Professor Albert Einstein

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.

bool withinTolerance(int value, [int min = 0, int max = 10]) {
  return min <= value && value <= max;
}
withinTolerance(5)  // true
withinTolerance(15) // false
withinTolerance(9, 7, 11) // true
withinTolerance(9, 7) // true

Naming parameters

Dart allows you to use named parameters to make the meaning of the parameters more clear in function calls.

bool withinTolerance({int value, int min = 0, int max = 10}) {
  return min <= value && value <= max;
}
withinTolerance(value: 9, min: 7, max: 11) // true
withinTolerance(value: 9, min: 7, max: 11) // true
withinTolerance(min: 7, value: 9, max: 11) // true
withinTolerance(max: 11, min: 7, value: 9) // true
withinTolerance(value: 5) // true
withinTolerance(value: 15) // false
withinTolerance(value: 5, min: 7) // false
withinTolerance(value: 15, max: 20) // true

Making named parameters required

The fact that named parameters are optional by default causes a problem. A person might look at your function declaration, assume that all of the parameters to the function are optional, and call your function like so:

print(withinTolerance());
NoSuchMethodError: The method '>' was called on null.
import 'package:meta/meta.dart';
dart pub get
bool withinTolerance({
  @required int value,
  int min = 0,
  int max = 10,
}) {
  return min <= value && value <= max;
}

Writing good functions

People have been writing code for decades. Along the way, they’ve designed 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 makes you fat, 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!

void hello() {
  print('Hello!');
}
String hello() {
  return 'Hello!';
}
var myPreciousData = 5782;

String anInnocentLookingFunction(String name) {
  myPreciousData = -1;
  return 'Hello, $name. Heh, heh, heh.';
}

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.

Choosing good names

You should always give your functions names that describe exactly what they do. If your code reads like well-written prose, it will be faster to read and easier for people to understand and to reason about.

Optional types

Earlier you saw this function:

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

Mini-exercises

  1. Write a function named youAreWonderful, with a String parameter called name. It should return a string using name, and say something like “You’re wonderful, Bob.”
  2. Add another int parameter to that function called numberPeople so that the function returns something like “You’re wonderful, Bob. 10 people think so.” Make both inputs named parameters.
  3. Make name required and set numberPeople to have a default of 30.

Anonymous functions

All the functions you’ve seen previously in this chapter, such as main, hello, and withinTolerance are named functions, which means, well, they have a name.

First-class citizens

In Dart, functions are first-class citizens. That means you can treat them like any other other type, assigning functions as values to variables and even passing functions around as parameters or returning them from other functions.

Assigning functions to variables

When assigning a value to a variable, functions behave just like other types:

int number = 4;
String greeting = 'hello';
bool isHungry = true;
Function multiply = (int a, int b) {
  return a * b;
};
Function myFunction = int multiply(int a, int b) {
  return a * b;
};
Function expressions can't be named.

Passing functions to functions

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

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

Returning functions from functions

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

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

Using anonymous functions

Now that you know where you can use anonymous functions, have a hand at doing it yourself. Take the multiply function from above again:

final multiply = (int a, int b) {
  return a * b;
};
print(multiply(2, 3));

Returning a function

Have a look at a different example:

Function applyMultiplier(num multiplier) {
  return (num value) {
    return value * multiplier;
  };
}
final triple = applyMultiplier(3);
print(triple(6));
print(triple(14.0));
18
42.0

Anonymous functions in forEach loops

Chapter 4 introduced you to forEach loops, which iterate over a collection. Although you may not have realized it, that was an example of using an anonymous function.

const numbers = [1, 2, 3];
numbers.forEach((number) {
  final tripled = number * 3;
  print(tripled);
});
3
6
9

Closures and scope

Anonymous functions in Dart act as what are known 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.

Function applyMultiplier(num multiplier) {
  return (num value) {
    return value * multiplier;
  };
}
var counter = 0;
final incrementCounter = () {
  counter += 1;
};
incrementCounter();
incrementCounter();
incrementCounter();
incrementCounter();
incrementCounter();
print(counter); // 5
Function countingFunction() {
  var counter = 0;
  final incrementCounter = () {
    counter += 1;
    return counter;
  };
  return incrementCounter;
}
final counter1 = countingFunction();
final counter2 = countingFunction();
print(counter1()); // 1
print(counter2()); // 1
print(counter1()); // 2
print(counter1()); // 3
print(counter2()); // 2

Mini-exercises

  1. Change the youAreWonderful function in the first mini-exercise of this chapter into an anonymous function. Assign it to a variable called wonderful.
  2. Using forEach, print a message telling the people in the following list that they’re wonderful.
const people = ['Chris', 'Tiffani', 'Pablo'];

Arrow functions

Dart has a special syntax for functions whose body is only one line. Consider the following named function add that adds two numbers together:

int add(int a, int b) {
  return a + b;
}
int add(int a, int b) => a + b;
(parameters) => expression;

Refactoring example 1

The body of the anonymous function you assigned to multiply has one line:

final multiply = (int a, int b) {
  return a * b;
};
final multiply = (int a, int b) => a * b;
print(multiply(2, 3)); // 6

Refactoring example 2

You can also use arrow syntax for the anonymous function returned by applyMultiplier:

Function applyMultiplier(num multiplier) {
  return (num value) {
    return value * multiplier;
  };
}
Function applyMultiplier(num multiplier) {
  return (num value) => value * multiplier;
}

Refactoring example 3

You can’t use arrow syntax on the forEach example, though:

numbers.forEach((number) {
  final tripled = number * 3;
  print(tripled);
});
numbers.forEach((number) => print(number * 3));

Mini-exercise

Change the forEach loop in the previous “You’re wonderful” mini-exercise to use arrow syntax.

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 if you get stuck in the challenge folder of this chapter.

Challenge 1: Prime time

Write a function that checks if a number is prime.

Challenge 2: Can you repeat that?

Write a function named repeatTask with the following definition:

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

Challenge 3: Darts and arrows

Update Challenge 2 to use arrow syntax.

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.
  • 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.
  • Anonymous functions act as closures, capturing any variables or functions within its scope.
  • Arrow syntax is a shorthand way to write one-line functions.

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.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.