Watch the video (11 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is a Higher-Order Function?
  5. 2. Reading a Function-Type Parameter
  6. 3. One Function, Many Rules
  7. 4. Passing an Arrow Function on the Spot
  8. 5. The Higher-Order Function Does the Shared Work
  9. 6. repeat — Passing an Action
  10. 7. Handing Values to the Function You Call
  11. 8. Calling the Function More Than Once
  12. 9. Dart Checks the Function's Shape
  13. 10. Returning a Function
  14. 11. Choosing Which Function to Return
  15. 12. A Higher-Order Function Over a List
  16. 13. A Real-World Example
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 36 — Higher-Order Functions

Category6 — Functions
Lecture036 of 140
Estimated video length11 min
TargetDart 3.12.2
LevelBeginner

Two functions that differ in a single line are still two functions to write, test, and fix. A higher-order function keeps the shared work in one place and takes the one line that changes as a function argument.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 001–035:

  • for and for-in loops, and switch expressions (Lecture 025)
  • functions with parameters and return values (Lectures 030–033)
  • arrow functions and anonymous functions (Lecture 034)
  • function types such as int Function(int), and passing a function as a value (Lecture 035)

Connection With Previous Lectures

Lecture 035 showed that a function is a value: it can be stored in a variable and passed around. But why would we want to? Look at these two functions from a shop's checkout:

dart
void checkoutTenPercent(double price) {
  double finalPrice = price * 0.9;
  print('Price: $price -> You pay: $finalPrice');
}

void checkoutFlatFifty(double price) {
  double finalPrice = price - 50;
  print('Price: $price -> You pay: $finalPrice');
}

Only the middle line differs. A third discount means a third copy, and a change to the receipt means editing every copy. This lecture removes the copies: write the checkout once, and pass the changing line in as a function.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what makes a function higher-order
  • Write a function that takes another function as a parameter
  • Read and write function-type parameters such as double Function(double) rule
  • Pass a named function or an arrow function as an argument
  • Call a function parameter once, many times, or with values of your own
  • Write a function that returns a function
  • Recognize the errors Dart gives when a function of the wrong shape is passed
  • Split a task into a fixed "how" (the higher-order function) and a changing "what" (the function you pass)

1. What Is a Higher-Order Function?

A higher-order function is a function that takes a function as a parameter, returns a function, or both. Every other function is an ordinary, "first-order" one.

Analogy — think of a coffee machine. It always heats the water, presses the coffee, and fills the cup. You only choose the recipe card you slide in. The machine is the higher-order function; the recipe card is the function you pass.

dart
double applyDiscount(double price, double Function(double) rule) {
  return rule(price);
}

double tenPercentOff(double price) => price * 0.9;

void main() {
  print(applyDiscount(200, tenPercentOff));
}
output
180.0

applyDiscount does not know any discount. It receives one as rule and calls it with rule(price).


2. Reading a Function-Type Parameter

The second parameter of applyDiscount reads left to right like this:

PartMeaning
doublethe passed function must return a double
Functionthis parameter holds a function
(double)the passed function must take one double
rulethe parameter's name inside applyDiscount

You call a function parameter exactly like any function: its name, then parentheses with arguments. Pass the function itself — tenPercentOff, with no parentheses. Writing tenPercentOff() would try to call it right there.


3. One Function, Many Rules

The payoff: the same applyDiscount works with every rule that has the right shape.

dart
double applyDiscount(double price, double Function(double) rule) {
  return rule(price);
}

double tenPercentOff(double price) => price * 0.9;
double flatFiftyOff(double price) => price - 50;
double noDiscount(double price) => price;

void main() {
  print(applyDiscount(200, tenPercentOff));
  print(applyDiscount(200, flatFiftyOff));
  print(applyDiscount(200, noDiscount));
}
output
180.0
150.0
200.0

A new discount is one new short function. applyDiscount never changes.


4. Passing an Arrow Function on the Spot

A rule used only once does not need a name. Write it right inside the call:

dart
double applyDiscount(double price, double Function(double) rule) {
  return rule(price);
}

void main() {
  print(applyDiscount(200, (price) => price * 0.75));
  print(applyDiscount(200, (price) => price / 2));
}
output
150.0
100.0

Notice that price has no type. Dart reads it from applyDiscount's parameter type — the rule must take a double, so price is a double.


5. The Higher-Order Function Does the Shared Work

Now we can rebuild the checkout from the start of the lecture. The receipt line and a safety check live in one place; the rule only does the maths.

dart
double applyDiscount(double price, double Function(double) rule) {
  double newPrice = rule(price);
  if (newPrice < 0) {
    newPrice = 0;
  }
  print('Price: $price -> You pay: $newPrice');
  return newPrice;
}

double tenPercentOff(double price) => price * 0.9;
double flatFiftyOff(double price) => price - 50;

void main() {
  applyDiscount(200, tenPercentOff);
  applyDiscount(30, flatFiftyOff);
}
output
Price: 200.0 -> You pay: 180.0
Price: 30.0 -> You pay: 0.0

30 - 50 would be -20, but no rule can ever produce a negative price, because the check sits in the higher-order function that every rule passes through.


6. repeat — Passing an Action

A function type with void and empty parentheses, void Function(), means "an action that takes nothing and returns nothing". That lets us write our own repeat:

dart
void repeat(int times, void Function() action) {
  for (int i = 0; i < times; i++) {
    action();
  }
}

void sayHello() => print('Hello!');

void main() {
  repeat(3, sayHello);
  repeat(2, () => print('Dart is fun'));
}
output
Hello!
Hello!
Hello!
Dart is fun
Dart is fun

The loop is written once. What happens in each round is the caller's choice.


7. Handing Values to the Function You Call

The higher-order function can pass its own values into the function it calls. Here repeat tells the action which round it is:

dart
void repeat(int times, void Function(int round) action) {
  for (int round = 1; round <= times; round++) {
    action(round);
  }
}

void main() {
  repeat(3, (round) => print('Lap $round done'));
}
output
Lap 1 done
Lap 2 done
Lap 3 done

The name inside void Function(int round) is optional. It is there for the reader — the type would mean the same written as void Function(int).


8. Calling the Function More Than Once

A function parameter can be called as often as you like, even on its own result:

dart
int applyTwice(int Function(int) step, int value) {
  return step(step(value));
}

int addTen(int n) => n + 10;
int doubleIt(int n) => n * 2;

void main() {
  print(applyTwice(addTen, 5));
  print(applyTwice(doubleIt, 5));
}
output
25
20

The inner step(value) runs first: addTen(5) is 15, then addTen(15) is 25.


9. Dart Checks the Function's Shape

A function parameter has a type, so Dart checks every function you pass. sayHello takes nothing and returns nothing — not what a discount rule needs:

dart
double applyDiscount(double price, double Function(double) rule) {
  return rule(price);
}

void sayHello() => print('Hello!');

void main() {
  print(applyDiscount(200, sayHello)); // ✗
}
output
Error: The argument type 'void Function()' can't be assigned to the parameter type 'double Function(double)'.
  print(applyDiscount(200, sayHello)); // ✗
                           ^

The message names both shapes, so you can see exactly what does not match.


10. Returning a Function

The other kind of higher-order function builds a function and hands it back. Its return type is a function type:

dart
int Function(int) makeMultiplier(int factor) {
  return (int n) => n * factor;
}

void main() {
  int Function(int) triple = makeMultiplier(3);
  int Function(int) tenTimes = makeMultiplier(10);
  print(triple(5));
  print(tenTimes(5));
  print(makeMultiplier(2)(7));
}
output
15
50
14

One function makes as many multipliers as we need. In the last line, makeMultiplier(2) returns a function, and (7) calls it straight away. Notice that triple still uses factor long after makeMultiplier has finished — how a function remembers that value is the subject of Lecture 037 — Closures & Scope.


11. Choosing Which Function to Return

A higher-order function can also pick from functions that already exist. A switch expression makes this neat:

dart
double tenPercentOff(double price) => price * 0.9;
double flatFiftyOff(double price) => price - 50;
double noDiscount(double price) => price;

double Function(double) pickRule(String code) {
  return switch (code) {
    'SAVE10' => tenPercentOff,
    'FLAT50' => flatFiftyOff,
    _ => noDiscount,
  };
}

void main() {
  double Function(double) rule = pickRule('FLAT50');
  print(rule(300));
  print(pickRule('HELLO')(300));
}
output
250.0
300.0

An unknown code falls through to _ and gets noDiscount, so the caller always receives a working rule.


12. A Higher-Order Function Over a List

A function that answers yes or no — bool Function(int) — is called a test. Passing one lets a single loop count anything:

dart
int countIf(List<int> numbers, bool Function(int) test) {
  int count = 0;
  for (int n in numbers) {
    if (test(n)) {
      count++;
    }
  }
  return count;
}

void main() {
  var marks = [85, 42, 67, 91, 38];
  print(countIf(marks, (mark) => mark >= 50));
  print(countIf(marks, (mark) => mark.isEven));
}
output
3
2

List<int> is the type of a list of whole numbers; lists get their own category starting with Lecture 039. Lists also come with higher-order functions built in — you already met forEach in Lecture 034, and map, where, and reduce also take a function, just like countIf. We will study them in Lectures 043 — List Transformation and 044 — List Aggregation.


13. A Real-World Example

Sara buys items worth 400 with the code SAVE10, and pays in three instalments:

dart
double tenPercentOff(double price) => price * 0.9;
double flatFiftyOff(double price) => price - 50;
double noDiscount(double price) => price;

double Function(double) pickRule(String code) {
  return switch (code) {
    'SAVE10' => tenPercentOff,
    'FLAT50' => flatFiftyOff,
    _ => noDiscount,
  };
}

double applyDiscount(double price, double Function(double) rule) {
  double newPrice = rule(price);
  if (newPrice < 0) {
    newPrice = 0;
  }
  return newPrice;
}

void repeat(int times, void Function(int round) action) {
  for (int round = 1; round <= times; round++) {
    action(round);
  }
}

void main() {
  double cart = 400;
  double total = applyDiscount(cart, pickRule('SAVE10'));
  print('Cart: $cart, Total: $total');
  repeat(3, (round) => print('Instalment $round: ${total / 3}'));
}
output
Cart: 400.0, Total: 360.0
Instalment 1: 120.0
Instalment 2: 120.0
Instalment 3: 120.0

Each function has one job: pickRule chooses, applyDiscount calculates safely, repeat loops, and the small arrow functions supply the details.


Code Examples

Example 1 — Wrapping an Action

dart
void runTask(void Function() task) {
  print('Start');
  task();
  print('End');
}

void main() {
  runTask(() => print('Working...'));
}
output
Start
Working...
End

Example 2 — A Row of Stars

dart
void repeat(int times, void Function() action) {
  for (int i = 0; i < times; i++) {
    action();
  }
}

void main() {
  repeat(3, () => print('* * *'));
}
output
* * *
* * *
* * *

Example 3 — Applying a String Rule Twice

dart
String applyTwice(String Function(String) step, String value) {
  return step(step(value));
}

String excite(String text) => '$text!';

void main() {
  print(applyTwice(excite, 'Hi Zayan'));
}
output
Hi Zayan!!

Example 4 — Passing an Operation

dart
int combine(int a, int b, int Function(int, int) operation) {
  return operation(a, b);
}

void main() {
  print(combine(3, 4, (a, b) => a + b));
  print(combine(3, 4, (a, b) => a * b));
}
output
7
12

Example 5 — Making Adders

dart
int Function(int) makeAdder(int amount) {
  return (int n) => n + amount;
}

void main() {
  int Function(int) addFive = makeAdder(5);
  print(addFive(10));
  print(makeAdder(100)(1));
}
output
15
101

Example 6 — Counting with a Test

dart
int countIf(List<int> numbers, bool Function(int) test) {
  int count = 0;
  for (int n in numbers) {
    if (test(n)) {
      count++;
    }
  }
  return count;
}

void main() {
  print(countIf([3, 8, 10, 7, 12], (n) => n > 5));
}
output
4

Example 7 — Student Marks

dart
void gradeAll(List<int> marks, String Function(int) grader) {
  for (int mark in marks) {
    print('$mark: ${grader(mark)}');
  }
}

String passFail(int mark) => mark >= 50 ? 'Pass' : 'Fail';

void main() {
  gradeAll([72, 45, 90], passFail);
}
output
72: Pass
45: Fail
90: Pass

How it works

  1. gradeAll owns the loop and the output format.
  2. passFail owns only the decision for one mark.
  3. A letter-grade version would be a new grader — gradeAll stays the same.

Example 8 — Shopping Total

dart
double checkout(double total, double Function(double) rule) {
  double finalTotal = rule(total);
  print('Ali pays $finalTotal');
  return finalTotal;
}

void main() {
  double total = 150 + 250 + 100;
  checkout(total, (t) => t > 400 ? t * 0.8 : t);
  checkout(300, (t) => t > 400 ? t * 0.8 : t);
}
output
Ali pays 400.0
Ali pays 300.0

How it works

  1. The rule gives 20% off totals over 400, and leaves smaller totals unchanged.
  2. 500 is over 400, so Ali pays 500 * 0.8, which is 400.0.
  3. 300 is not, so the rule returns it as it is.

Example 9 — Bank Balance

dart
int Function(int) deposit(int amount) {
  return (int balance) => balance + amount;
}

int applyTo(int balance, int Function(int) action) {
  int newBalance = action(balance);
  print('Balance: $balance -> $newBalance');
  return newBalance;
}

void main() {
  int balance = 1000;
  balance = applyTo(balance, deposit(500));
  balance = applyTo(balance, (b) => b - 200);
}
output
Balance: 1000 -> 1500
Balance: 1500 -> 1300

How it works

  1. deposit(500) returns a function that adds 500 to any balance.
  2. applyTo takes that function, calls it, prints, and returns the result.
  3. The withdrawal is an arrow function written on the spot.

Example 10 — Temperature Conversion

dart
void convertAll(List<double> temps, double Function(double) convert) {
  for (double t in temps) {
    print('$t -> ${convert(t)}');
  }
}

double toFahrenheit(double c) => c * 9 / 5 + 32;

void main() {
  convertAll([0, 25, 100], toFahrenheit);
}
output
0.0 -> 32.0
25.0 -> 77.0
100.0 -> 212.0

How it works

  1. [0, 25, 100] is passed where a List<double> is expected, so each value is a double.
  2. convertAll prints each temperature beside its converted value.
  3. Passing a toKelvin function instead would reuse the same loop.

Common Beginner Mistakes

1. Calling the function instead of passing it

dart
// double tenPercentOff(double price) => price * 0.9;
// applyDiscount(200, tenPercentOff(200));
output
Error: The argument type 'double' can't be assigned to the parameter type 'double Function(double)'.

tenPercentOff(200) runs the rule and passes its result, 180.0. Pass tenPercentOff with no parentheses.

2. Passing a function of the wrong shape

dart
// void sayHello() => print('Hello!');
// applyDiscount(200, sayHello);
output
Error: The argument type 'void Function()' can't be assigned to the parameter type 'double Function(double)'.

The passed function must match the parameter's return type and its parameter types.

3. Forgetting the parentheses when calling the parameter

dart
void repeat(int times, void Function() action) {
  for (int i = 0; i < times; i++) {
    action; // ✗ names the function, never calls it
  }
}

void main() {
  repeat(3, () => print('Hello!'));
  print('Finished');
}
output
Finished

No error, and no Hello! either. action alone only mentions the function. Write action().

4. Forgetting the return type of a returned function

makeMultiplier(int factor) without int Function(int) in front becomes dynamic, and Dart can no longer check how its result is called. Always write the function type.


Best Practices

  • Use a higher-order function when several functions differ only in one small step.
  • Name function parameters by their role: rule, action, test, convert.
  • Keep the shared, safety-critical work (checks, output) in the higher-order function.
  • Use arrow functions for one-off arguments; name a function when you reuse it.
  • Always write full function types, such as bool Function(int), for parameters and returns.
  • Don't make a function higher-order "just because" — one fixed behaviour needs no parameter.

Mini Challenge

Write int applyN(int times, int Function(int) step, int start) that starts with start and applies step to it times times. Use it to give Zayan's score of 50 three bonuses of 10, and to double 1 five times.

Your program should print exactly:

output
80
32

Try it yourself before opening the solution.

Show solution
dart
int applyN(int times, int Function(int) step, int start) {
  int value = start;
  for (int i = 0; i < times; i++) {
    value = step(value);
  }
  return value;
}

void main() {
  print(applyN(3, (score) => score + 10, 50));
  print(applyN(5, (n) => n * 2, 1));
}
output
80
32

How it works

  1. value starts as start and is replaced by step(value) each round.
  2. 50 → 60 → 70 → 80, and 1 → 2 → 4 → 8 → 16 → 32.
  3. applyN knows nothing about scores or doubling — the step decides.

Practice Questions

Work through these in order — they run from beginner to application to challenge. Every question gives you exact values, so you can compare your output with the answers.

Beginner

  1. Write void runTwice(void Function() action) that calls action twice. Call it with an arrow function that prints Hi Ali.
  2. Write int applyRule(int value, int Function(int) rule) and a named function addOne. Print applyRule(9, addOne).
  3. Write void repeat(int times, void Function(int round) action) and use it to print Step 1, Step 2, and Step 3.

Application

  1. Write String format(String name, String Function(String) style). For 'Sara', print the result with a style that upper-cases the name, then with one that returns Dr. Sara.
  2. Write int calculate(int a, int b, int Function(int, int) op). For 20 and 4, print the difference and the whole-number division (~/).
  3. Using countIf from section 12, count how many of [45, 72, 50, 38, 90] are at least 50.
  4. Using applyDiscount from section 1, print a 250 price with 20% off, and with a flat 30 off — both as arrow functions.
  5. Write String Function(String) makeGreeter(String greeting). Store makeGreeter('Hello') and call it with 'Zayan' to print Hello, Zayan!.

Challenge

  1. Write int Function(int, int) pickOperation(String symbol) with a switch expression for '+', '-', and '*', returning a function that gives 0 for anything else. Print pickOperation('*')(6, 7), pickOperation('-')(6, 7), and pickOperation('?')(6, 7).
  2. Write int retry(int maxTries, bool Function(int attempt) tryOnce). It calls tryOnce with 1, 2, 3, … and returns the first attempt number that gives true, or -1 if all maxTries fail. Test it with (attempt) => attempt * 25 >= 70 and 5 tries, then with (attempt) => false and 3 tries.

Practice Answers

Question 1 — Solution

dart
void runTwice(void Function() action) {
  action();
  action();
}

void main() {
  runTwice(() => print('Hi Ali'));
}
output
Hi Ali
Hi Ali

Question 2 — Solution

dart
int applyRule(int value, int Function(int) rule) {
  return rule(value);
}

int addOne(int n) => n + 1;

void main() {
  print(applyRule(9, addOne));
}
output
10

Question 3 — Solution

dart
void repeat(int times, void Function(int round) action) {
  for (int round = 1; round <= times; round++) {
    action(round);
  }
}

void main() {
  repeat(3, (round) => print('Step $round'));
}
output
Step 1
Step 2
Step 3

Question 4 — Solution

dart
String format(String name, String Function(String) style) {
  return style(name);
}

void main() {
  print(format('Sara', (n) => n.toUpperCase()));
  print(format('Sara', (n) => 'Dr. $n'));
}
output
SARA
Dr. Sara

Question 5 — Solution

dart
int calculate(int a, int b, int Function(int, int) op) {
  return op(a, b);
}

void main() {
  print(calculate(20, 4, (a, b) => a - b));
  print(calculate(20, 4, (a, b) => a ~/ b));
}
output
16
5

Question 6 — Solution

dart
int countIf(List<int> numbers, bool Function(int) test) {
  int count = 0;
  for (int n in numbers) {
    if (test(n)) {
      count++;
    }
  }
  return count;
}

void main() {
  print(countIf([45, 72, 50, 38, 90], (mark) => mark >= 50));
}
output
3

Question 7 — Solution

dart
double applyDiscount(double price, double Function(double) rule) {
  return rule(price);
}

void main() {
  print(applyDiscount(250, (price) => price * 0.8));
  print(applyDiscount(250, (price) => price - 30));
}
output
200.0
220.0

Question 8 — Solution

dart
String Function(String) makeGreeter(String greeting) {
  return (String name) => '$greeting, $name!';
}

void main() {
  String Function(String) hello = makeGreeter('Hello');
  print(hello('Zayan'));
}
output
Hello, Zayan!

Question 9 — Solution

dart
int Function(int, int) pickOperation(String symbol) {
  return switch (symbol) {
    '+' => (int a, int b) => a + b,
    '-' => (int a, int b) => a - b,
    '*' => (int a, int b) => a * b,
    _ => (int a, int b) => 0,
  };
}

void main() {
  print(pickOperation('*')(6, 7));
  print(pickOperation('-')(6, 7));
  print(pickOperation('?')(6, 7));
}
output
42
-1
0

Question 10 — Solution

dart
int retry(int maxTries, bool Function(int attempt) tryOnce) {
  for (int attempt = 1; attempt <= maxTries; attempt++) {
    if (tryOnce(attempt)) {
      return attempt;
    }
  }
  return -1;
}

void main() {
  print(retry(5, (attempt) => attempt * 25 >= 70));
  print(retry(3, (attempt) => false));
}
output
3
-1

25 and 50 are below 70; 75 is not, so attempt 3 succeeds. The early return ends the loop at once, and -1 is reached only when every attempt fails.


Quick Revision

  1. What two things can make a function higher-order?
  2. How do you read the type bool Function(int)?
  3. Why do you pass tenPercentOff and not tenPercentOff()?
  4. Where does the type of price come from in applyDiscount(200, (price) => price / 2)?
  5. What does void Function() describe?
  6. What does makeMultiplier(2)(7) do, step by step?
  7. What happens if a function parameter is written as action; instead of action();?

Key Takeaways

  • A higher-order function takes a function as a parameter, returns one, or both.
  • It keeps the shared "how" in one place; the passed function supplies the changing "what".
  • A function-type parameter states the return type and parameter types: double Function(double).
  • Pass a function by its name with no parentheses, or write an arrow function on the spot.
  • The higher-order function decides when, how often, and with what values to call it.
  • Dart checks the shape of every function you pass and names both shapes in the error.
  • A function can build and return a new function, such as makeMultiplier(3).
  • Lists have built-in higher-order functions (forEach, map, where, reduce) — coming in Lectures 043–044.

What's Next?

Lecture 037 — Closures & Scope. makeMultiplier(3) finished running long ago, yet triple still knows that factor is 3. Next we look at scope — which variables a piece of code can see — and at closures, the functions that carry their variables with them.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.