Chapter 4. Functions

Functions are the workhorse of Dart. It is functions that actually compute things. We use the term functions loosely to cover functions and methods of all syntactic forms, regardless of whether they behave as mathematical functions or not.

Functions in Dart are first class values that can be stored in variables, passed as parameters and returned as results. Like all runtime values in Dart, functions are objects.

We have already encountered a variety of functions: top-level functions, instance and class methods (be they getters, setters, operators or vanilla methods) and constructors. As we shall see, there are also local functions and function literals. They all share common structure which we will illustrate here.

4.1 Parameters

A function always has a list of formal parameters, though the parameter list may be empty, and for getter methods it is elided. Parameters are either positional or named.

4.1.1 Positional Parameters

Parameters may be required or optional. Here are some functions with required parameters:

zero() => 0; // a function with no parameters; technically, it has 0 parameters
get zero => 0; // another version of the same thing
id(x) => x; // The identity function;
identity(x) { return x;} // a more verbose identity function
add(a, b) => a + b; // a function with two required parameters

Optional parameters must all be listed together at the end of the parameter list in between square brackets. Any required parameters must precede the optional ones. Optional parameters can be given default values, which must be compile time constants.

increment(x, [step = 1]) => x + step; // step is optional, defaults to 1

We can call increment() with either one or two arguments.

increment(1); // evaluates to 2
increment(2, 4); // evaluates to 6
increment(); // runtime error
increment(2, 3, 4); // runtime error

The first call binds x to 1; since the second argument was not provided, step gets bound to its default value of 1, yielding 1 + 1 = 2. The second call obviously yields 2 + 4 = 6. The third call raises a NoSuchMethodError when executed. Even though we have a function named increment in scope, we don’t have one that takes zero arguments, and so an exception is thrown. The situation with respect to the final call is similar; there is no version of increment() that accepts three parameters.

4.1.2 Named Parameters

Parameters may be positional or named. We have already seen many examples of positional parameters. Named parameters are less common in programming languages, which is why we have held back on introducing them until now. Named parameters are declared after any positional parameters, between curly braces.

Here is an example that relies exclusively on named parameters:

addressLetter({name: '', street: '', number, city, zip, country}) {
 var addr = new Address();
 addr.street = street;
 addr.number = number;
 addr.city = city;
 addr.zip = zip;
 addr.country = country;
 return addr;
}
addressLetter(street: "Downing", number: 10);
addressLetter(street: "Wall", city: "New York", country: "USA");
addressLetter(city: "Paris", country: "France");
addressLetter(name: "Alice", country: "Wonderland");
addressLetter();
addressLetter(name: "room", number: 101, country: "Oceania");

As the code shows, one can invoke addressLetter() with any combination of named parameters—including all or none.

The following example shows a case where required and named parameters are mixed.

var map = new Map();
fail() => throw('Key not found'),
lookup(key, {ifMissing: fail}) {
  var result = map[key];
  if (result == null) {return ifMissing();}
  return result;
}

Often, a callback function such as an error handler is specified via a named parameter, while the other formals are given positionally.

lookup("anything"); // throws
lookup("anything", ifMissing: () => map["anything"] = 42); // 42

If we wanted to give a better error message, we might want to write

var map = new Map();
lookup(key, {ifMissing}) {
  var result = map[key];
  if (result != null) {return result;}
  return ifMissing == null? throw '$key not found' : ifMissing();
}

In this variant, we take advantage of the fact that if no default value is specified for an optional parameter, it will default to null.

Named parameters are always optional. In other words, the classification of parameters as required vs. optional is not orthogonal to their classification as positional vs. named. You cannot mix optional positional parameters with named parameters—you either use one kind or the other.

Formal parameters are not final. They can be mutated just like any other variable; however this should be avoided as a matter of good style. One should seek to minimize the use of mutable variables; mutation makes code harder to understand and reason about.

4.2 Function Bodies

A function has a body containing code to be evaluated when the function is executed. The body follows the signature, and can come in the two forms we’ve seen:

1. A list of statements (possibly empty) enclosed in curly braces.

2. A single expression following the token =>.

In the first form, execution begins with the first statement in the body and continues until the first of the following things happens: either the last statement in the function has been successfully executed, a return statement is executed, or an exception that is not caught inside the function is thrown.

Every function in Dart either returns a value or throws an exception.

If we finish the last statement and it is not a return, we return null from the function. For example

sideEffect(){
  print("I don't have a return and I don't throw an exception");
  print("What is my value?");
}
sideEffect(); // prints and then evaluates to null

Constructors are special functions used to create instances of classes. Constructors include factories, which are normal functions accorded a special role, and generative constructors. Generative constructors differ in that they always return a fresh instance or throw an exception. So a generative constructor does not return null in the absence of an explicit return statement. Indeed, a generative constructor cannot return an arbitrary expression. It may only include a return statement if that statement has no associated expression.

4.3 Function Declarations

Most of the functions we’ve seen so far have been introduced by function declarations. The exceptions have been constructors, getters and setters. A function declaration has a name that is followed by a parameter list and a body.

Abstract methods consist of a function signature but provide no body. Abstract methods are not technically function declarations. They serve only as declarations that help guide the static analyzer.

Function declarations can occur at the top level (e.g., main()) or as methods, and we have encountered many examples. However, functions may also be local functions. Local functions are function definitions nested within other functions.

Here is a simple example—defining a nested auxiliary function when computing the Fibonacci sequence:

fib(n) {
  lastTwo(n) {
    if (n < 1) { return [0, 1]; }
    else {
      var p = lastTwo(n-1);
      return [p[1], p[0]+p[1]];
    }
  }
  return lastTwo(n)[1];
}

This isn’t the best way to compute the nth Fibonacci number but it does avoid the waste of the naive recursive version. Since lastTwo() is just an implementation detail of fib(), it is best to nest lastTwo() inside fib() and avoid polluting the surrounding namespace with an extra function name.

4.4 Closures

Functions can be defined inline as part of an expression. These are known as function literals or, more loosely, as closures. Unlike function declarations, closures are not named. However, they do have a parameter list and a body just like any other function.

(x) => x; // yet another identity function
(x) {return x;} // and another
(x, [step = 1]) => x + step; // a closure with an optional parameter
(a, b) => a + b; // a closure with two required parameters

Looking at these examples in isolation, it may not be immediately clear how useful closures are. They look exactly like function declarations without names. The real benefit comes from using them as part of a larger expression.

Consider the problem of summing the elements of a list. One can certainly write a for loop to do this, but that is a rather primitive approach. A knowledgeable Dart programmer would instead write

sum(nums) => nums.reduce( (a, b) => a + b);

The reduce() method is defined for lists and many other types in Dart. Anything that implements Iterable should have a working reduce() method. The method takes a binary function, which we shall call combiner, as its argument. When invoked, reduce iterates over its receiver. Processing starts by taking the first two elements and feeding them to combiner. In each subsequent iteration, the result of the last call to combiner is passed as the first argument to a new call to combiner, along with the next element of the receiver. If combiner() adds its arguments, the effect is to add the first two elements of the receiver, then add the result to the third element and so on, producing the sum.

We could of course write sum without closures

sum(nums) { // bad style
  plus(a,b) => a + b;
  nums.reduce(plus);
}

which is slightly more verbose and requires us to name the addition function. In the case of addition, the name is obvious, but having to declare and name a function becomes increasingly onerous in examples like

country.cities.where((city) => city.population > 1000000);

which presumably finds all the cities in country with a population of over a million people. This snippet uses the where() method, another method from Iterable that takes a function as an argument. In this case, the argument must be a unary predicate, and where() will return those elements for which the predicate holds true.

4.5 Invoking Methods and Functions

Functions can be invoked in the standard way, by following a function valued expression with a parenthesized argument list, for example, print(‘Hello, oh brave new world that has such people in it’).

Some functions, known as getters, can be invoked without a parameter list:

true.runtimeType; // bool

As we discussed earlier, the uniform use of getters and setters provides us the valuable property of representation independence. One should note, however, that unlike several other languages that support representation independence, Dart does not fully adhere to the principle of uniform reference. In Dart, one does not refer to methods and fields via a uniform syntax. While getters and fields are treated indistinguishably, ensuring representation independence, getters and methods are accessed via different syntax. Consequently, Dart programmers need to be aware if a function is declared as a getter or a nullary method. A similar argument holds for setters.

4.5.1 Cascades

In addition to the normal use of the dot operator for member selection, Dart supports the double-dot operator for method cascades. Cascades are useful when we need to perform a series of operations on a given object.

A cascade is evaluated just like an ordinary method invocation, except that it does not evaluate to the value returned by the method invocation, but rather to the receiver of the method invocation.

"Hello".length.toString(); // evaluates to '5'
"Hello"..length.toString(); // evaluates to "Hello"

Therefore, instead of

var address = new Address.of("Freddy Krueger");
address.setStreet("Elm", "13a");
address.city = "Carthage";
address.state = "Eurasia";
address.zipCode(66666, extend: 66666);
address;

we can write a single expression

new Address.of("Freddy Krueger")
 ..setStreet("Elm", "13a")
 ..city = "Carthage"
 ..state = "Eurasia"
 ..zipCode(66666, extend: 66666);

Cascades do more than just save a few keystrokes. They enable the creation of fluent APIs without preplanning. In the absence of cascades, methods must be designed to always return the receiver so that they can be chained. With cascades, we achieve the same effect as chaining independently of the return values of the individual methods in the API.

It’s important to format cascades carefully so that the code is readable. One should not abuse the cascade syntax by putting a series of cascades on a single line or by excessive nesting.

Cascades are useful when working with so-called builder APIs, where a descriptor is created in steps, but the object being described is created in one fell swoop at the end of the building process.

Another use for cascades arises when we want to invoke a method on an object and get the original target back, but the method returns some other result. If we write

var sortedColors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3).sort();

we will find that sortedColors is null because sort() is void. We could rearrange the code

var colors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3);
colors.sort();

but is much nicer to simply use a cascade

var sortedColors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3)..sort();

4.5.2 Assignment

It may not be obvious, but assignments in Dart are often function invocations, because assignment to fields is a sugar for setter method invocation.

The precise meaning of an assignment such as v = e depends on the declaration of v. If v is a local variable or a parameter, it is just an old fashioned assignment. Otherwise, the assignment is a sugar for invoking the setter named v=.

Whether an assignment is valid will depend on whether the setter v is defined, or whether the variable v is final. Final variables may not be reassigned and do not induce setters.

Compound assignments such as i += 2 are defined in terms of ordinary assignment. See Section 6.1.6 for more details.

4.5.3 Using Operators

Dart supports user-defined operators, such as the + operator we defined for Point. User defined operators in Dart are actually instance methods that use special names and have special syntax when used. Declarations of such methods must be prefixed with the built-in identifier operator.

Apart from syntax, all the rules that pertain to instance methods apply to operators. The set of permissible operators is: <, >, <=, >=, ==, -, +, /, ~/, *, %, |, ^, &, <<, >>, []=, [], ~.

In addition, there are a number of fixed operators that cannot be defined by the programmer. These are &&, || and the increment and decrement operators ++ and --(both prefix and postfix).

Assignment isn’t considered an operator for these purposes, though the compound assignments do rely on operators for their semantics.

Identity isn’t an operator in Dart either. Instead, Dart provides the predefined function identical().

The precedence rules for operators are fixed and follow established conventions. Likewise their arity. Most operators are binary. The notable exceptions are unary minus and []=. The latter is used for situations similar to assignment into arrays or maps (or any indexed collection), and requires two arguments: an index and a new value. The case of - is special, since we support both binary and unary minus operations.

4.6 The Function Class

Function is an abstract class that acts as a common superinterface for all functions. Function does not have any instance methods declared. It does however declare the class method apply(), which takes a function and an argument list, and invokes the named function with the arguments supplied.

The signature of apply() is

static apply(Function function,
  List positionalArguments,
  [Map<Symbol, dynamic> namedArguments]
  );

You’ll note that the formal parameters of apply() carry type annotations. It requires a function to call and a list of positional arguments (which might be empty). Named arguments may be provided via a map from names to actual arguments, which could be objects of any type. The last argument is optional. Most functions don’t take any named arguments, so it is convenient if they need not be provided.

The apply() method provides a mechanism for calling functions with a dynamically determined argument list. This way, we can deal with argument lists whose arity is unknown at compile time.

4.6.1 Emulating Functions

As mentioned before, it is a key tenet of object-oriented programming that it is the behavior of an object that matters, not its provenance. Ideally, any object should be able to emulate any other. For example, instances of Proxy are designed to emulate the behavior of arbitrary objects. Since functions are objects, we should be able to emulate their behavior as well. How could we emulate a function with Proxy? The most common and important behavior for a function is what it does when invoked, but invocation is a built-in operation. We’d like to be able to write

var p = new Proxy((x) => x*2);
p(1); // we want 2

Happily, the code above works as expected, though nothing we’ve discussed so far explains why.

It turns out that function application translates to an invocation of a special method named call(). All true functions implicitly support a call() method whose signature is the same as the one declared by the function, and whose effect is to execute the function. In the example, p(1) is really p.call(1). Of course, we cannot treat p.call(1) as p.call.call(1) otherwise we’d recurse forever.

Since Proxy has no method call, noSuchMethod() is invoked, forwarding the call to the target of the proxy, which, being a function, has its own call() method.

Any class that declares a call() method is considered to implicitly implement class Function.

Note that Function does not declare a call() method. The reason is that there is no specific signature it could declare: call() can have differing arity and may or may not have optional arguments (be they positional or named) with varying defaults. So there really is no common declaration of call() that could be listed in Function. Instead, the call() method is treated specially by the language.

4.7 Functions as Objects

Dart is a purely object-oriented language and so all runtime values in Dart are objects, functions included. Functions support the methods declared in Object, in addition to the call() method described in the previous section.

In most cases, functions inherit the methods of Object unchanged. Implementations have some freedom with respect to the implementation of toString() on functions. They typically produce a reasonable description of the function, which may include its signature.

Because the implementations of == and hashCode are usually inherited, two functions are usually considered equal only if they are identical. It is difficult to do otherwise, as a general notion of semantic equality among functions is undecidable.

How do we know that two functions are identical? Let us look at some examples.

computeIdentityFunction() {
 return (x) => x;
}

Can we assume that two distinct invocations of computeIdentityFunction() yield identical results? The answer is no. While this case it seems “obvious” that the function is the same, in fact, a distinct function object may be allocated each time a function expression is evaluated. Similarly, a local function declaration introduces a new object in each dynamic scope that contains it. As an example, consider the local function increment in the code below.

makeCounter() {
 var counter = 0;
 increment() => ++counter;
 return increment;
}

Each call to makeCounter() returns a different increment function. In this case, it is clear we don’t want the same function object, since each is tied to a different counter variable.

A top-level or static function declaration can be named, and that name always denotes the same object.

However, in some cases we can do better. Dart treats closures derived via property extraction specially. If (and only if!) two expressions o1 and o2 evaluate to the same object (i.e., o1 and o2 are identical), then Dart will ensure that for a given identifier m, if o1.m is legal, then o1.m == o2.m.

As an example, consider typical UI code that registers listener functions with the DOM. We want to be able to deregister a function after we have registered it. If we registered a regular closure as a listener

myElement.onClick.listen((event) => listener(event));

we could not deregister it later via a call such as

myElement.removeEventListener('onClick', (event) => listener(event));

because the closure we pass to removeEventListener() is not the same as the one we registered. Even though they look the same, each is a distinct object, and there is no general way to determine if two distinct function objects are equal. We would have to store the listener so we could use it to deregister later. However, using property extraction

myElement.onClick.listen(myObject.listener);

deregistration works as expected

myElement.removeEventListener('onClick', myObject.listener);

The implementation of noSuchMethod() is inherited from Object, as is that of runtimeType. Various classes may be used to represent functions in a Dart runtime. All these classes will implement Function but one cannot reliably expect equality or identity to hold between the runtime types of different functions.

f(x) { return (x) => x;}

g() {
  h() => 42;
  h.runtimeType is Function; // true
  h.runtimeType == (() => 42).runtimeType; // don't bank on this being true
  g.runtimeType == h.runtimeType; // or this
  (() => 42).runtimeType == (() => 42).runtimeType; // or even this!
}


main(){
 f.runtimeType is Function; // true
 f(3).runtimeType is Function; // true
 g.runtimeType is Function; // true
 f(2).runtimeType == f.runtimeType; // maybe true, maybe not
 f.runtimeType == g.runtimeType; // unlikely as arities differ, but no promises
}

As the code above shows, one cannot be sure if the runtime types of any two functions are the same. What one can rely upon is that they all implement Function. Of course, equal functions have equal runtime types.

f(x) { return (x) => x;}


g() {
  h() => 42;
  h.runtimeType == h.runtimeType; // true
  var x = (() => 42);
  x.runtimeType == x.runtimeType; // true
}


main(){
 f.runtimeType == f.runtimeType; // true
 g.runtimeType == g.runtimeType; // true
}

4.8 Generator Functions

Dart supports generators, which are functions that are used to generate values in a collection. Generators may be synchronous or asynchronous. Synchronous generators provide a syntactic sugar for producing iterators (4.8.1), whereas asynchronous generators do the same for streams (8.3).

Here we will focus on synchronous generators. We will defer discussion of asynchronous generators (8.6.2) to Chapter 8 which deals with all things asynchronous.

4.8.1 Iterators and Iterables

Iterators are objects that allow one to iterate over a collection in sequence, and nothing else. Iterators are especially convenient if one wants to produce the contents of a collection lazily. Collections that support iteration via iterators are known as iterables. Iterables must have a getter iterator that returns an iterator.

The for-in (6.2.3.1) loop will operate on any iterable object.

The interfaces of iterators and iterables are codified by the classes Iterator and Iterable respectively.

Producing an iterator is rather formulaic. One needs to define an iterable collection class and in particular one must define its iterator getter that returns (obviously) an iterator. That will, in turn, require you to define an iterator class, with a moveNext() method. As an example, here is a particularly excruciating way to print the natural numbers up to 20:

class NaturalsIterable {
 var n;
 NaturalsIterable.to(this.n);
 get iterator => new NaturalIterator(n);
}

class NaturalIterator {
 var n;
 var current = -1;
 NaturalIterator(this.n);
 moveNext() {
  if (current < n) {
    current++;
    return true;
    }
  return false;
 }
}

naturalsTo(n) => new NaturalsIterable.to(n);

main() {
 for (var i in naturalsTo(20)) {print(i);}
}

In fact, a well-typed example would implement the full Iterable interface and would be a good deal longer.

4.8.2 Synchronous Generators

To reduce the boilerplate engendered by iterators, Dart supports synchronous generator functions. Using a synchronous generator saves us the trouble of defining the two classes necessary to implement even the most basic iterator. We can define a synchronous generator function by marking its body with the modifier sync*:

naturalsTo(n) sync* {
 var k = 0;
 while (k < n) yield k++;
}

When called, the function immediately returns an Iterable i from which one can obtain an iterator j. The first time someone calls moveNext() on j, the function body begins execution. Upon entering the loop, the yield statement (6.2.9) is executed, causing k to be incremented, appending the prior value of k to i and suspending execution of naturalsTo(). The next time moveNext() is called, execution of naturalsTo() resumes just after the yield and the loop repeats.1

1. There are better ways to implement this particular example; when the elements of the sequence are computed based upon natural numbers, there is a convenient constructor, Iterable.generate() that takes an integer n and a function f, and produces an iterable representing the sequence f(0) . . . f(n – 1)):

naturalsTo(n) => new Iterable.generate(n, (x) => x);

The point of our example, however, is to show all the machinery necessary when defining iterators in the general case, and how sync* methods can simplify that machinery.

It is important to understand that the body of a generator function executes after the function has returned a result to its caller. It is natural to ask what role does a return statement (6.2.8) play inside a generator? The answer is that return simply terminates the generator.2

2. In some cases, return may not cause termination because a finally clause (6.2.4) my change the flow of control.

There is no question as to the disposition of any value being returned. One cannot have a return statement return a value in a generator; such a statement will be flagged as an error by the compiler. It would not make any sense to allow such a statement, given that a value has already been returned to the caller, and the caller has already completed processing and has disappeared from the call stack.

Even though it runs after the function has returned a result to the caller, the function body is associated with the result and interacts with it. In a synchronous generator, the result is always an iterable. The generator is always associated with both that iterable and an iterator derived from it.

Yield statements inside a synchronous generator append objects to its associated iterable, and then suspend the body as shown above. Execution and resumption of the body is always initiated by a call to moveNext() on the associated iterator. When yield suspends the body, moveNext() returns true to its caller. When the generator terminates, moveNext() returns false.

4.9 Related Work

Functions as first-class values have a vast and illustrious history in programming languages. It is far beyond the scope of this book to explore the space of functional languages, which use such function values as their fundamental building block.

The notion of treating functions as objects goes back to Smalltalk[1] whose blocks are precursors of Dart closures. Smalltalk blocks originally suffered various restrictions which have been removed in modern dialects.

A key difference between Dart functions and its ancestors in the Smalltalk language family is the behavior of return inside a closure. Smalltalk supports non-local returns, meaning that a return executed within a closure exits the surrounding method, returning to that method’s caller. As a result, it is possible to define control constructs as library functions that take functions as parameters. This is not possible in Dart. The decision to avoid non-local returns in Dart was made regretfully due to implementation limitations on the underlying web platform. See 6.2.8 for further details.

Notably, Scala[15] also supports non-local returns.

4.10 Summary

Dart functions are objects like all other runtime values in Dart. Functions in Dart may be declared to accept positional or named parameters. Positional parameters may be required or optional. Named parameters are always optional.

Functions are always lexically scoped and close over their environment. However, Dart functions are not suited to the implementation of user-defined control constructs due to the semantics of return.

Dart supports functions both as methods within classes and as independent structures. Methods may be associated with an instance (instance methods) or a class (class methods). Independent functions may be declared at the library level (top-level functions), as local functions within other functions, or via literal expressions.

All built-in operators are also functions, and most of them are defined as instance methods that may be overridden by the programmer. User-defined classes can be defined to behave like built-in function types by implementing the special method call. All Dart functions are considered to be members of the type Function.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.107.254