[Dart Language Tour] Functions, Typedefs

Functions, Typedefs

Functions

Dart is a true object-oriented language, so even functions are objects and have a type, Function. This means that functions can be assigned to variables or passed as arguments to other functions. You can also call an instance of a Dart class as if it were a function. For details, see Callable classes.

Here’s an example of implementing a function:

1
2
3
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}

Although Effective Dart recommends type annotations for public APIs, the function still works if you omit the types:

1
2
3
isNoble(atomicNumber) {
return _nobleGases[atomicNumber] != null;
}

For functions that contain just one expression, you can use a shorthand syntax:

1
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

The => expr syntax is a shorthand for { return expr; }. The => notation is sometimes referred to as arrow syntax.


Note: Only an expression—not a statement—can appear between the arrow (=>) and the semicolon (;). For example, you can’t put an if statement there, but you can use a conditional expression.


Parameters

A function can have any number of required positional parameters. These can be followed either by named parameters or by optional positional parameters (but not both).


Note: Some APIs — notably Flutter widget constructors — use only named parameters, even for parameters that are mandatory. See the next section for details.


You can use trailing commas when you pass arguments to a function or when you define function parameters.

Named parameters

Named parameters are optional unless they’re specifically marked as required.

When calling a function, you can specify named parameters using paramName: value. For example:

1
enableFlags(bold: true, hidden: false);

When defining a function, use {param1, param2, …} to specify named parameters:

1
2
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold, bool hidden}) {...}

Although named parameters are a kind of optional parameter, you can annotate them with @required to indicate that the parameter is mandatory — that users must provide a value for the parameter. For example:

1
const Scrollbar({Key key, @required Widget child})

If someone tries to create a Scrollbar without specifying the child argument, then the analyzer reports an issue.

To use the @required annotation, depend on the meta package and import package:meta/meta.dart.

Optional positional parameters

Wrapping a set of function parameters in [] marks them as optional positional parameters:

1
2
3
4
5
6
7
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}

Here’s an example of calling this function without the optional parameter:

1
assert(say('Bob', 'Howdy') == 'Bob says Howdy');

And here’s an example of calling this function with the third parameter:

assert(say(‘Bob’, ‘Howdy’, ‘smoke signal’) ==
‘Bob says Howdy with a smoke signal’);

Default parameter values

Your function can use = to define default values for both named and positional parameters. The default values must be compile-time constants. If no default value is provided, the default value is null.

Here’s an example of setting default values for named parameters:

1
2
3
4
5
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold will be true; hidden will be false.
enableFlags(bold: true);

Deprecation note: Old code might use a colon (:) instead of = to set default values of named parameters. The reason is that originally, only : was supported for named parameters. That support might be deprecated, so we recommend that you use = to specify default values.


The next example shows how to set default values for positional parameters:

1
2
3
4
5
6
7
8
String say(String from, String msg,
[String device = 'carrier pigeon']) {
var result = '$from says $msg with a $device';
return result;
}

assert(say('Bob', 'Howdy') ==
'Bob says Howdy with a carrier pigeon');

You can also pass lists or maps as default values. The following example defines a function, doStuff(), that specifies a default list for the list parameter and a default map for the gifts parameter.

1
2
3
4
5
6
7
8
9
10
void doStuff(
{List<int> list = const [1, 2, 3],
Map<String, String> gifts = const {
'first': 'paper',
'second': 'cotton',
'third': 'leather'
}}) {
print('list: $list');
print('gifts: $gifts');
}

The main() function

Every app must have a top-level main() function, which serves as the entrypoint to the app. The main() function returns void and has an optional List parameter for arguments.

Here’s an example of the main() function for a web app:

1
2
3
4
5
void main() {
querySelector('#sample_text_id')
..text = 'Click me!'
..onClick.listen(reverseText);
}

Note: The .. syntax in the preceding code is called a cascade. With cascades, you can perform multiple operations on the members of a single object.

Here’s an example of the main() function for a command-line app that takes arguments:

1
2
3
4
5
6
7
8
// Run the app like this: dart args.dart 1 test
void main(List<String> arguments) {
print(arguments);

assert(arguments.length == 2);
assert(int.parse(arguments[0]) == 1);
assert(arguments[1] == 'test');
}

You can use the args library to define and parse command-line arguments.

Functions as first-class objects

You can pass a function as a parameter to another function. For example:

1
2
3
4
5
6
7
8
9
10
11
12
void printElement(int element) {
print(element);
}

var list = [1, 2, 3];

// Pass printElement as a parameter.
list.forEach(printElement);
You can also assign a function to a variable, such as:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

This example uses an anonymous function. More about those in the next section.

Anonymous functions

Most functions are named, such as main() or printElement(). You can also create a nameless function called an anonymous function, or sometimes a lambda or closure. You might assign an anonymous function to a variable so that, for example, you can add or remove it from a collection.

An anonymous function looks similar to a named function— zero or more parameters, separated by commas and optional type annotations, between parentheses.

The code block that follows contains the function’s body:

1
2
3
([[Type] param1[, …]]) {
codeBlock;
};

The following example defines an anonymous function with an untyped parameter, item. The function, invoked for each item in the list, prints a string that includes the value at the specified index.

1
2
3
4
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});

Click Run to execute the code.

If the function contains only one return statement, you can shorten it using arrow notation. Paste the following line into DartPad and click Run to verify that it is functionally equivalent.

1
2
list.forEach(
(item) => print('${list.indexOf(item)}: $item'));

Lexical scope

Dart is a lexically scoped language, which means that the scope of variables is determined statically, simply by the layout of the code. You can “follow the curly braces outwards” to see if a variable is in scope.

Here is an example of nested functions with variables at each scope level:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool topLevel = true;

void main() {
var insideMain = true;

void myFunction() {
var insideFunction = true;

void nestedFunction() {
var insideNestedFunction = true;

assert(topLevel);
assert(insideMain);
assert(insideFunction);
assert(insideNestedFunction);
}
}
}

Notice how nestedFunction() can use variables from every level, all the way up to the top level.

Lexical closures

A closure is a function object that has access to variables in its lexical scope, even when the function is used outside of its original scope.

Functions can close over variables defined in surrounding scopes. In the following example, makeAdder() captures the variable addBy. Wherever the returned function goes, it remembers addBy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
return (int i) => addBy + i;
}

void main() {
// Create a function that adds 2.
var add2 = makeAdder(2);

// Create a function that adds 4.
var add4 = makeAdder(4);

assert(add2(3) == 5);
assert(add4(3) == 7);
}

Testing functions for equality

Here’s an example of testing top-level functions, static methods, and instance methods for equality:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void foo() {} // A top-level function

class A {
static void bar() {} // A static method
void baz() {} // An instance method
}

void main() {
var x;

// Comparing top-level functions.
x = foo;
assert(foo == x);

// Comparing static methods.
x = A.bar;
assert(A.bar == x);

// Comparing instance methods.
var v = A(); // Instance #1 of A
var w = A(); // Instance #2 of A
var y = w;
x = w.baz;

// These closures refer to the same instance (#2),
// so they're equal.
assert(y.baz == x);

// These closures refer to different instances,
// so they're unequal.
assert(v.baz != w.baz);
}

Return values

All functions return a value. If no return value is specified, the statement return null; is implicitly appended to the function body.

1
2
3
foo() {}

assert(foo() == null);

Typedefs

In Dart, functions are objects, just like strings and numbers are objects. A typedef, or function-type alias, gives a function type a name that you can use when declaring fields and return types. A typedef retains type information when a function type is assigned to a variable.

Consider the following code, which doesn’t use a typedef:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SortedCollection {
Function compare;

SortedCollection(int f(Object a, Object b)) {
compare = f;
}
}

// Initial, broken implementation.
int sort(Object a, Object b) => 0;

void main() {
SortedCollection coll = SortedCollection(sort);

// All we know is that compare is a function,
// but what type of function?
assert(coll.compare is Function);
}

Type information is lost when assigning f to compare. The type of f is (Object, Object) → int (where → means returns), yet the type of compare is Function. If we change the code to use explicit names and retain type information, both developers and tools can use that information.

typedef Compare = int Function(Object a, Object b);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SortedCollection {
Compare compare;

SortedCollection(this.compare);
}

// Initial, broken implementation.
int sort(Object a, Object b) => 0;

void main() {
SortedCollection coll = SortedCollection(sort);
assert(coll.compare is Function);
assert(coll.compare is Compare);
}

Note: Currently, typedefs are restricted to function types. We expect this to change.


Because typedefs are simply aliases, they offer a way to check the type of any function. For example:

typedef Compare = int Function(T a, T b);

1
2
3
4
5
int sort(int a, int b) => a - b;

void main() {
assert(sort is Compare<int>); // True!
}

References

[1] Language tour | Dart - https://dart.dev/guides/language/language-tour

[2] Effective Dart: Design | Dart - https://dart.dev/guides/language/effective-dart/design