Emulating Functions in Dart
Written by Gilad Bracha
January 2012 (updated September 2018)
This document describes how to define Dart classes that behave like functions.
The call() method
The following example has an ordinary class, WannabeFunction
, that
defines a method named call()
.
class WannabeFunction { int call(int a, int b) => a + b; }
The call()
method is special, in that anyone who defines a call()
method is
presumed to emulate a function. This allows us to use instances of
WannabeFunction
as if they were functions that take two integer arguments
and return an integer:
var wf = WannabeFunction(); assert(wf(3, 4) == 7);
The example above is rather trivial, and we would be better off writing a function directly. However, there are cases where this ability can be quite useful. It is also core to the design philosophy of the Dart language:
- What matters about an object is its behavior. If object a has a procedural interface that is compatible with that of another object b, a may substitute for b.
- The interface of any kind of object can always be emulated by another suitably defined object.
How does it work?
When x(a1, .., an)
is evaluated,
if it is a normal
function, it gets called in the normal way. If it isn’t we just invoke call()
on it. If x
supports a call()
method with suitable arguments it gets called.
Otherwise, noSuchMethod()
gets invoked.
Assignment is different.
When an object that supports call()
is assigned to a target that
has a function type, the object is converted to a function object.
More precisely, when e
(an expression that has a type with a call
method)
is assigned to a variable
(or passed as a parameter, or returned,
or used to initialize something, etc.)
and the type of the target is a function type,
then e
is implicitly transformed to e.call
—
a tear-off operation that yields a function object.
The apply() method
The class Function defines the static method apply()
with the following signature:
static apply(Function function, List positionalArguments, [Map<Symbol, dynamic> namedArguments]);
The apply() function allows functions to be called in generic fashion. The last argument is positional, and is only needed if the function we mean to call takes named arguments. These are provided via map from argument names to their values. One thing to pay attention to is that names are described via instances of class Symbol.
Symbols
You can create symbols from strings:
new Symbol('myFavoriteMethodName');
If possible, create constant symbol objects:
const Symbol('myFavoriteMethodName');
Using constant symbols helps dart2js minify your code.
Interactions with mirrors and noSuchMethod()
In Dart, you can customize how objects react to methods that are not explicitly defined in their class chain by overriding noSuchMethod(). Here’s an example showing how you could use function emulation inside noSuchMethod():
@override dynamic noSuchMethod(Invocation invocation) { return invocation.memberName == #foo ? Function.apply( baz, invocation.positionalArguments, invocation.namedArguments) : super.noSuchMethod(invocation); }
The first branch handles the case where you want to forward just the parameters to
another function. If you know baz
doesn’t take any named arguments,
then that code can instead be
Function.apply(baz, invocation.positionalArguments)
. The second branch simply forwards
to the standard implementation of the noSuchMethod(), a common pattern.
The only argument to noSuchMethod() is an Invocation. The boolean properties of Invocation identify the syntactic form of the method invocation, as the following table shows.
Form of method invocation | |||
---|---|---|---|
x.y | x.y = e | x.y(...) | |
isMethod | false | false | true |
isGetter | true | false | false |
isSetter | false | true | false |
isAccessor | true | true | false |
It is important not to assume that isMethod
means that a non-accessor was
being looked up, since in fact, Dart semantics mean that we would have called
noSuchMethod() only if neither a normal method nor a getter were found.
Likewise, isGetter
does not imply a getter was being looked up; if a method
was present, it would be closurized and returned.
Summary
Here is what you need to know to define a class that works similarly to a function in Dart:
- Define a class with a method named call.
- Implement the
call()
method to define what instances of your class do when invoked as functions via the () syntax.