Watch the video (11 min)
On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What "First-Class" Means
  5. 2. Storing a Function in a Variable
  6. 3. add versus add(2, 3)
  7. 4. Function Types
  8. 5. Dart Checks Function Types
  9. 6. Storing an Anonymous Function
  10. 7. Passing a Function as an Argument
  11. 8. Returning a Function
  12. 9. Choosing Behaviour at Runtime
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 35 — Functions as First-Class Objects

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

Until now a function could only be called. In Dart a function is also a value: you can store it in a variable, hand it to another function, and choose which one runs while the program is running.

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–034:

  • variables, var, type inference, and dynamic (Lectures 004, 005, 011)
  • if statements and switch expressions (Lectures 022–025)
  • functions with typed parameters and return types (Lectures 030–031)
  • arrow functions => and anonymous functions (Lecture 034)

Connection With Previous Lectures

Here is a program that prints a result with one of two calculations:

dart
int add(int a, int b) => a + b;
int multiply(int a, int b) => a * b;

void main() {
  String choice = 'add';
  if (choice == 'add') {
    print('Result: ${add(6, 3)}');
  } else {
    print('Result: ${multiply(6, 3)}');
  }
}
output
Result: 9

The print line is written twice; only the function differs. Add subtract and divide and the copies multiply. What we really want is one line that says "print the result of whichever function was chosen". For that, the chosen function itself must be something we can keep in a variable — a value, just like 9 or 'add'.


Learning Objectives

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

  • Explain what "functions are first-class objects" means
  • Store a function in a variable using a tear-off (no ())
  • Tell the difference between add and add(2, 3)
  • Write function types such as int Function(int, int)
  • Pass a function as an argument to another function
  • Return a function from a function
  • Choose which function runs while the program is running
  • Recognize the errors Dart gives for mismatched function types

1. What "First-Class" Means

A value is first-class when you can do everything with it that you can do with a number or a string: store it in a variable, pass it to a function, and return it from a function. In Dart, functions are first-class.

Analogy — a function is like a recipe card. You can cook the recipe (call the function), but you can also hand the card to someone else, who cooks it later. Handing the card over does not cook anything.


2. Storing a Function in a Variable

Write the function's name without parentheses. This is called a tear-off: you tear the function off and keep it, without running it.

dart
void greet(String name) {
  print('Hello, $name!');
}

void main() {
  var sayHello = greet; // stored, not called
  sayHello('Ali');
  sayHello('Sara');
}
output
Hello, Ali!
Hello, Sara!

sayHello now holds the same function as greet. Calling sayHello('Ali') runs greet's body with name set to 'Ali'.


3. add versus add(2, 3)

The parentheses make all the difference:

  • add is the function itself — the recipe card.
  • add(2, 3) calls it and gives back the result — the cooked meal.
dart
int add(int a, int b) {
  return a + b;
}

void main() {
  print(add);
  print(add(2, 3));
}
output
Closure: (int, int) => int from Function 'add': static.
5

Printing the function shows its shape: it takes (int, int) and returns int. Dart calls function values "closures" in this message. We will see where that word comes from in Lecture 037.


4. Function Types

Every function value has a type, and Dart writes it like this:

output
returnType Function(parameterTypes)

So add has the type int Function(int, int), and greet has void Function(String). You can write that type on a variable instead of var:

dart
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;

void main() {
  int Function(int, int) operation = add;
  print(operation(10, 4));
  operation = subtract;
  print(operation(10, 4));
}
output
14
6

The variable can be reassigned to any function with the same shape. Here the same line operation(10, 4) gives a different answer depending on which function it holds.


5. Dart Checks Function Types

A function type is checked like any other type. A function with the wrong shape is rejected before the program runs:

dart
int square(int n) => n * n;

void main() {
  int Function(int, int) operation = square; // ✗
  print(operation(2, 3));
}
output
Error: A value of type 'int Function(int)' can't be assigned to a variable of type 'int Function(int, int)'.
  int Function(int, int) operation = square; // ✗
                                     ^

square takes one int, but the variable promises two. Even with var, Dart infers the function type and protects it — calling sayHello(25) from section 2 would fail with "The argument type 'int' can't be assigned to the parameter type 'String'".


6. Storing an Anonymous Function

In Lecture 034 we wrote functions without names. A variable is a natural home for them:

dart
void main() {
  var doubleIt = (int n) => n * 2;
  String Function(String) shout = (String text) {
    return '${text.toUpperCase()}!';
  };

  print(doubleIt(21));
  print(shout('hello'));
}
output
42
HELLO!

Named or anonymous, it does not matter: once stored, both are called the same way.


7. Passing a Function as an Argument

A parameter can have a function type too. The caller then decides which function to hand over:

dart
int add(int a, int b) => a + b;
int multiply(int a, int b) => a * b;

void showResult(int a, int b, int Function(int, int) operation) {
  print('Result: ${operation(a, b)}');
}

void main() {
  showResult(6, 3, add);
  showResult(6, 3, multiply);
  showResult(6, 3, (a, b) => a - b);
}
output
Result: 9
Result: 18
Result: 3

This solves the problem from the start of the lecture: showResult has one print line, and the caller chooses the calculation. Notice add with no () — we hand the card over; showResult cooks it. In the last call, Dart knows from the parameter type that a and b are int, so the anonymous function needs no types.

A function that takes or returns another function is called a higher-order function. Lecture 036 is all about them.


8. Returning a Function

A function can also give back a function. Its return type is a function type:

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

double Function(double) converterFor(String unit) {
  if (unit == 'F') {
    return toFahrenheit;
  }
  return toKelvin;
}

void main() {
  var convert = converterFor('F');
  print(convert(25));
  print(converterFor('K')(25));
}
output
77.0
298.15

converterFor('F') returns the function toFahrenheit, which we store and then call. The last line does both at once: the first () gets a function, the second () calls it.


9. Choosing Behaviour at Runtime

Put it together with a switch expression from Lecture 025, and the program picks a function based on data it only knows while running:

dart
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
int multiply(int a, int b) => a * b;

int Function(int, int) pick(String symbol) {
  return switch (symbol) {
    '+' => add,
    '-' => subtract,
    _ => multiply,
  };
}

void main() {
  String symbol = '-';
  var operation = pick(symbol);
  print('20 $symbol 8 = ${operation(20, 8)}');
}
output
20 - 8 = 12

Change symbol to '+' and the same code prints 20 + 8 = 28. No if chain repeats the print; the decision is made once, and the chosen function is just a value from then on.


10. A Real-World Example

dart
double noDiscount(double total) => total;
double tenPercentOff(double total) => total * 0.9;
double flatFiveHundredOff(double total) => total >= 2000 ? total - 500 : total;

void checkout(String customer, double total, double Function(double) discount) {
  print('$customer pays ${discount(total)}');
}

void main() {
  checkout('Ali', 1500, noDiscount);
  checkout('Sara', 1500, tenPercentOff);
  checkout('Zayan', 2500, flatFiveHundredOff);
}
output
Ali pays 1500.0
Sara pays 1350.0
Zayan pays 2000.0

checkout does not know or care which discount rule applies — it receives the rule as a double Function(double) and calls it. A new rule is just a new function; checkout never changes.


Code Examples

Example 1 — A Tear-Off in a Variable

dart
void welcome(String name) {
  print('Welcome, $name!');
}

void main() {
  var hello = welcome;
  hello('Zayan');
}
output
Welcome, Zayan!

Example 2 — An Explicit Function Type

dart
bool isAdult(int age) => age >= 18;

void main() {
  bool Function(int) check = isAdult;
  print(check(20));
  print(check(6));
}
output
true
false

Example 3 — Reassigning a Function Variable

dart
String polite(String name) => 'Good morning, $name.';
String casual(String name) => 'Hey $name!';

void main() {
  String Function(String) greeting = polite;
  print(greeting('Sara'));
  greeting = casual;
  print(greeting('Sara'));
}
output
Good morning, Sara.
Hey Sara!

Example 4 — An Anonymous Function in a Variable

dart
void main() {
  int Function(int) triple = (n) => n * 3;
  print(triple(7));
}
output
21

Example 5 — A Function Parameter

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

void sayHi() {
  print('Hi!');
}

void main() {
  repeatTwice(sayHi);
}
output
Hi!
Hi!

void Function() is the type of a function with no parameters that returns nothing.

Example 6 — Returning a Function

dart
String morning(String name) => 'Good morning, $name';
String evening(String name) => 'Good evening, $name';

String Function(String) greeterFor(int hour) {
  if (hour < 12) {
    return morning;
  }
  return evening;
}

void main() {
  print(greeterFor(9)('Ali'));
  print(greeterFor(19)('Ali'));
}
output
Good morning, Ali
Good evening, Ali

Example 7 — Student Marks

dart
String strictGrade(int marks) => marks >= 80 ? 'Pass' : 'Fail';
String easyGrade(int marks) => marks >= 50 ? 'Pass' : 'Fail';

void report(String name, int marks, String Function(int) grader) {
  print('$name: ${grader(marks)}');
}

void main() {
  report('Ali', 65, strictGrade);
  report('Ali', 65, easyGrade);
}
output
Ali: Fail
Ali: Pass

How it works

  1. Both graders have the type String Function(int).
  2. report receives one of them and calls it with the marks.
  3. The same 65 gives different results because a different rule was passed in.

Example 8 — Shopping Total

dart
double withTax(double price) => price * 1.05;
double withoutTax(double price) => price;

double Function(double) priceRuleFor(bool isTaxFree) {
  return isTaxFree ? withoutTax : withTax;
}

void main() {
  var priceRule = priceRuleFor(false);
  print('Notebook: ${priceRule(200)}');
  print('Bag: ${priceRule(1000)}');
}
output
Notebook: 210.0
Bag: 1050.0

How it works

  1. The conditional expression picks a function, not a number — no () after either name.
  2. priceRuleFor(false) returns withTax, and priceRule is then used for every item.
  3. Calling priceRuleFor(true) would change every price without touching the print lines.

Example 9 — Bank Balance

dart
int deposit(int balance, int amount) => balance + amount;
int withdraw(int balance, int amount) => balance - amount;

int apply(int balance, int amount, int Function(int, int) action) {
  int result = action(balance, amount);
  print('Balance: $result');
  return result;
}

void main() {
  int balance = 1000;
  balance = apply(balance, 500, deposit);
  balance = apply(balance, 300, withdraw);
}
output
Balance: 1500
Balance: 1200

How it works

  1. deposit and withdraw share the shape int Function(int, int).
  2. apply runs whichever action it is given and returns the new balance.
  3. main stores each returned balance and passes it into the next step.

Example 10 — Temperature Conversion

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

double Function(double) converterFor(String unit) {
  return switch (unit) {
    'F' => toFahrenheit,
    _ => toKelvin,
  };
}

void main() {
  String unit = 'F';
  double celsius = 30;
  print('$celsius°C = ${converterFor(unit)(celsius)}°$unit');
}
output
30.0°C = 86.0°F

How it works

  1. The switch expression returns a function based on unit.
  2. converterFor(unit) gives back toFahrenheit; the second (celsius) calls it.
  3. 30 * 9 / 5 + 32 is 86.0.

Common Beginner Mistakes

1. Calling the function instead of passing it

dart
// int add(int a, int b) => a + b;
// showResult(4, 2, add(4, 2)); // ✗ passes the number 6
output
Error: The argument type 'int' can't be assigned to the parameter type 'int Function(int, int)'.

add(4, 2) runs the function and passes its result. To pass the function, write add.

2. Adding empty () to a tear-off

dart
// showResult(4, 2, add()); // ✗
output
Error: Too few positional arguments: 2 required, 0 given.

add() is still a call — just one with missing arguments. A tear-off has no parentheses at all.

3. A function with the wrong shape

dart
// int square(int n) => n * n;
// int Function(int, int) operation = square; // ✗
output
Error: A value of type 'int Function(int)' can't be assigned to a variable of type 'int Function(int, int)'.

The number of parameters, their types, and the return type must all match.

4. Reassigning a var function variable to a different shape

dart
// var greet = (String name) => 'Hi, $name';
// greet = (int n) => 'Number $n'; // ✗
output
Error: A value of type 'String Function(int)' can't be assigned to a variable of type 'String Function(String)'.

As in Lecture 005, var fixes the type from the first value — and that includes a function type.

5. Using the bare type Function

dart
int add(int a, int b) => a + b;

void main() {
  Function operation = add; // compiles, but checks nothing
  // print(operation('2', '3')); // ✗ crashes at run time
}
output
Unhandled exception:
type 'String' is not a subtype of type 'int' of 'a'

Plain Function means "some function", so Dart cannot check the arguments — it behaves like dynamic. Write the full type, int Function(int, int).


Best Practices

  • Pass functions as tear-offs — add, not add() or (a, b) => add(a, b).
  • Write full function types (int Function(int, int)), never bare Function.
  • Use var for function variables when the value makes the type obvious.
  • Give functions you pass around clear names that describe the rule: tenPercentOff, strictGrade.
  • Reach for a function parameter when two pieces of code differ only in one step.
  • Long function types get hard to read. Lecture 071 shows how a typedef gives them a short name.

Mini Challenge

Write two functions, celsiusToFahrenheit and fahrenheitToCelsius, each of type double Function(double). Write void showConversion(double value, double Function(double) convert) that prints <value> -> <result>. Call it once with each function, using 100 both times.

Your program should print exactly:

output
100.0 -> 212.0
100.0 -> 37.77777777777778

Try it yourself before opening the solution.

Show solution
dart
double celsiusToFahrenheit(double c) => c * 9 / 5 + 32;
double fahrenheitToCelsius(double f) => (f - 32) * 5 / 9;

void showConversion(double value, double Function(double) convert) {
  print('$value -> ${convert(value)}');
}

void main() {
  showConversion(100, celsiusToFahrenheit);
  showConversion(100, fahrenheitToCelsius);
}
output
100.0 -> 212.0
100.0 -> 37.77777777777778

How it works

  1. Both converters take a double and return a double, so both fit the parameter type.
  2. Each call passes a tear-off — no ().
  3. showConversion calls whichever function it received.

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 cheer(String name) that prints Go, <name>!. Store it in a variable called shout and call shout('Sara').
  2. Write the function type of each: int half(int n), bool isEmpty(String s), void beep(), double area(double w, double h).
  3. What does each line print, and why are they different? print(isAdult(20)); and print(isAdult); where bool isAdult(int age) => age >= 18;

Application

  1. Declare int Function(int) operation. Assign (n) => n + 10, print operation(5), then assign (n) => n * 10 and print operation(5) again.
  2. Write void runThreeTimes(void Function() action) and use it to print Tick three times.
  3. Write int calculate(int a, int b, int Function(int, int) op) that returns op(a, b). Print the result for 12 and 4 with an add tear-off, then with (a, b) => a ~/ b.
  4. Write String Function(String) formatterFor(bool loud) that returns a function making text upper-case when loud is true, and lower-case otherwise. Print formatterFor(true)('Hello') and formatterFor(false)('Hello').
  5. This line does not compile. Explain why in one sentence, then fix it: bool Function(int) check = (String s) => s.isEmpty;

Challenge

  1. Write int Function(int, int) pick(String symbol) using a switch expression for '+', '-', '*' (anything else: a function that returns 0). Print 9 * 7 = 63 using pick('*').
  2. Write two bonus rules, int smallBonus(int marks) => marks + 5; and int bigBonus(int marks) => marks + 10;. Write int finalMarks(int marks, int Function(int) bonus) that applies the bonus but never returns more than 100. Print the result for Ali (88, big bonus) and Zayan (70, small bonus) as Ali: 98 and Zayan: 75, then for Sara (95, big bonus).

Practice Answers

Question 1 — Solution

dart
void cheer(String name) {
  print('Go, $name!');
}

void main() {
  var shout = cheer;
  shout('Sara');
}
output
Go, Sara!

Question 2 — Solution

  • int half(int n) → int Function(int)
  • bool isEmpty(String s) → bool Function(String)
  • void beep() → void Function()
  • double area(double w, double h) → double Function(double, double)

Question 3 — Solution

dart
bool isAdult(int age) => age >= 18;

void main() {
  print(isAdult(20));
  print(isAdult);
}
output
true
Closure: (int) => bool from Function 'isAdult': static.

isAdult(20) calls the function and prints its result. isAdult without () is the function itself, so Dart prints a description of it.

Question 4 — Solution

dart
void main() {
  int Function(int) operation = (n) => n + 10;
  print(operation(5));
  operation = (n) => n * 10;
  print(operation(5));
}
output
15
50

Question 5 — Solution

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

void tick() {
  print('Tick');
}

void main() {
  runThreeTimes(tick);
}
output
Tick
Tick
Tick

Question 6 — Solution

dart
int add(int a, int b) => a + b;

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

void main() {
  print(calculate(12, 4, add));
  print(calculate(12, 4, (a, b) => a ~/ b));
}
output
16
3

Question 7 — Solution

dart
String loudText(String text) => text.toUpperCase();
String quietText(String text) => text.toLowerCase();

String Function(String) formatterFor(bool loud) {
  if (loud) {
    return loudText;
  }
  return quietText;
}

void main() {
  print(formatterFor(true)('Hello'));
  print(formatterFor(false)('Hello'));
}
output
HELLO
hello

Question 8 — Solution

The variable expects a function that takes an int, but the anonymous function takes a String. Make the types agree — for example, check an int:

dart
void main() {
  bool Function(int) check = (int n) => n == 0;
  print(check(0));
}
output
true

Question 9 — Solution

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

void main() {
  print('9 * 7 = ${pick('*')(9, 7)}');
}
output
9 * 7 = 63

Each arm returns an anonymous function; Dart knows from the return type that a and b are int.

Question 10 — Solution

dart
int smallBonus(int marks) => marks + 5;
int bigBonus(int marks) => marks + 10;

int finalMarks(int marks, int Function(int) bonus) {
  int result = bonus(marks);
  if (result > 100) {
    return 100;
  }
  return result;
}

void main() {
  print('Ali: ${finalMarks(88, bigBonus)}');
  print('Zayan: ${finalMarks(70, smallBonus)}');
  print('Sara: ${finalMarks(95, bigBonus)}');
}
output
Ali: 98
Zayan: 75
Sara: 100

Sara's 95 + 10 is 105, so the early return caps it at 100.


Quick Revision

  1. What does it mean that functions are "first-class"?
  2. What is a tear-off, and how do you write one?
  3. What is the difference between add and add(2, 3)?
  4. What is the function type of String greet(String name)?
  5. Can a variable of type int Function(int, int) hold a function that takes one int?
  6. How do you write a parameter that accepts a function with no parameters and no result?
  7. In converterFor('F')(25), what does each pair of parentheses do?
  8. Why is the bare type Function a poor choice?

Key Takeaways

  • In Dart, functions are values: they can be stored, passed, and returned.
  • A tear-off is a function name without () — it refers to the function without calling it.
  • add is the function; add(2, 3) is its result.
  • A function type is written returnType Function(parameterTypes), e.g. int Function(int, int).
  • Dart checks function types: parameter count, parameter types, and return type must match.
  • A parameter with a function type lets the caller choose one step of the work.
  • A function can return a function; f(x)(y) gets a function and then calls it.
  • Choosing a function with if, ?:, or a switch expression lets a program pick its behaviour at runtime.
  • Avoid bare Function — it switches type checking off, like dynamic.

What's Next?

Lecture 036 — Higher-Order Functions. We have just seen that a function can take another function as an argument, or return one. Functions that do this have a name — higher-order functions — and they are one of the most useful tools in Dart. Next we study how to design them, and why so much of Dart is built around them.


Video lecture

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