Learning Dart
上QQ阅读APP看书,第一时间看更新

Using functions in Dart

Functions are another tool for changing the control flow; a certain task is delegated to a function by calling it and providing some arguments. A function does the requested task and returns a value; the control flow returns where the function was called. In Java and C#, classes are indispensable and they are the most important structuring concept.

But Dart is both functional and object oriented. Functions are first-class objects themselves (they are of type Function) and can exist outside of a class as top-level functions (inside a class they are called methods). In prorabbits_v2.dart of Chapter 1, Dart – A Modern Web Programming Language, calculateRabbits is an example of a top-level function; and deposit, withdraw, and toString from banking_v2.dart of this chapter are methods, to be called on as an object of the class. Don't create a static class only as a container for helper functions!

Return types

A function can do either of the following:

  • Do something, wherein the return type, if indicated, is void, for example, the display function in return_types.dart. In fact, such a function does return an object, namely null (see the print in line (1) of the following code).
  • Return an expression exp resulting in an object different from null, explicitly indicated by a return exp, as in displayStr (line (2)).

The { return exp; } syntax can be shortened to => exp; as shown in display and displayStrShort; we'll use this function expression syntax wherever possible. exp is an expression, but it cannot be a statement like if. A function can be an argument to another function, as display in print, line (1), or in line (4) where the function isOdd is passed to the function where:

main() {
  print(display('Hello')); // Message: Hello.   null   (1)
  print(displayStr('Hello')); // Message: Hello.       (2)
  print(displayStrShort('Hello')); // Message: Hello.
  print(display(display("What's up?")));               (3)
  [1,2,3,4,5].where(isOdd).toList();     // [1, 3, 5]  (4)  
}

display(message) => print('Message: $message.');

displayStr(message) {
  return 'Message: $message.';
}

displayStrShort(message) => 'Message: $message.';
isOdd(n) => n % 2 == 1;

}

By omitting the parameter type, the display function is more general; its argument can be a String, num, Boolean, List, and so on.

Parameters

As all parameter variables are objects, all parameters are passed by reference; that means that the underlying object can be changed from within the function. Two types of parameters exist: the required (they come first in the parameter list), and the optional parameters. Optional parameters that depend on their position in the list are indicated between [] in the definition of the function. All parameters we have seen so far in examples were required, but usage of only optional parameter(s) is also possible, as shown in the following code (refer to parameters.dart):

webLanguage([name]) =>  'The best web language is: $name';

When called as shown in the following code, it produces the output shown as comments:

print(webLanguage());  // The best web language is: null
print(webLanguage('JavaScript')); // The best web language is: // JavaScript

An optional parameter can have a default value as shown in the following code:

webLanguage2([name='Dart']) =>  'The best web language is: $name';

If this function is called without argument, the optional value is substituted instead, but when called with an argument, this takes precedence:

print(webLanguage2());  // The best web language is: Dart
print(webLanguage2('JavaScript')); // The best web language is:  // JavaScript

An example with required and optional parameters, with or without default values (name=value), is as follows:

String hi(String msg, [String from, String to])
                    => '$msg from $from to $to';
String hi2(String msg, [String from='me', String to='you'])
                    => '$msg from $from to $to';

Here msg always gets the first parameter value, from and to get a value when there are more parameters in that order (for that reason they are called positional):

print(hi('hi'));                  // hi from null to null
print(hi('hi', 'me'));            // hi from me to null
print(hi('hi', 'me', 'you'));     // hi from me to you
print(hi2('hi'));                 // hi from me to you
print(hi2('hi', 'him'));          // hi from him to you
print(hi2('hi', 'him', 'her'));   // hi from him to her

When calling a function with optional parameters it is often not clear what the code is doing. This can be improved by using named optional parameters. These are indicated by { } in the parameter list, such as in hi3:

String hi3(String msg, {String from, String to}) =>'$msg from $from to $to';

They are called with name:value and because of the name the position does not matter:

print(hi3('hi', to:'you', from:'me')); // hi from me to you

Named parameters can also have default values (name:value):

String hi4(String msg, {String from:'me', String to:'you'}) =>'$msg from $from to $to';

It is called as follows:

print(hi4('hi', from:'you')); // hi from you to you

The following list summarizes the parameters:

  • Optional positional parameters: [param]
  • Optional positional parameters with default values: [param=value]
  • Optional named parameters: {param}
  • Optional named parameters with default values: {param:value}

First class functions

A function can contain other functions, as calcRabbits contains calc(years) in prorabbits_v4.dart:

String calculateRabbits(int years) {
  calc(years) => (2 * pow(E, log(GROWTH_FACTOR) * years)).round().toInt();
  var out = "After $years years:\t ${calc(years)} animals";
  return out;
}

This can be useful if the inner function needs to be called several times within the outer function, but it cannot be called from outside this outer function. A slight variation is to store the function in a variable calc that has type Function, as in prorabbits_v5.dart:

String calculateRabbits(int years) {
  var calc = (years) => (2 * pow(E, log(GROWTH_FACTOR) * years)).round().toInt(); (1)
  assert(calc is Function);
  var out = "After $years years:\t ${calc(years)} animals";
  return out;
}

The right-hand side of line (1) is an anonymous function or lambda that takes parameter years and returns the expression after => (the lambda operator). It could also have been written as follows:

var calc2 = (years) {
  return (2 * pow(E, log(GROWTH_FACTOR) * years)).round().toInt();      
};

In prorabbits_v6.dart, the function calc is made top-level and is passed in the function lineOut as a parameter named fun:

void main() {
  print("The number of rabbits increases as:\n");
  for (int years = 0; years <= NO_YEARS; years++) {
    lineOut(years, calc(years));
  }
}

calc(years) => // code omitted, same as line(1) //in the preceding code

lineOut(yrs, fun) {
  print("After $yrs years:\t ${fun} animals");
}

As a variation to the previous code, prorabbits_v7.dart has the inner function calc, which has no parameter and yet it can use the variable years that exists in the surrounding scope. For that reason calc is called a closure; it closes over the surrounding variables, retaining their values.

String calculateRabbits(int years) {
 calc() => (2 * pow(E, log(GROWTH_FACTOR) * years)).round().toInt();
  
  var out = "After $years years:\t ${calc()} animals";
  return out;
}

Closures can also be defined as top-level functions, as closure.dart shows. The function multiply returns a function (that itself takes a parameter i). So mult2 in the following code is a function that needs to be called with a parameter, for example, mult2(3):

// short version:  multiply(num n) => (num i) => n * i;
// long version:
Function multiply(num n) {
 return (num i) => n * i;
}

main() {
  var two = 2;
  var mult2 = multiply(two); // this is called partial application
  assert(mult2 is Function);
  print('${mult2(3)}'); // 6
}

This closure behavior (true lexical scoping) is most clearly seen in closure2.dart, where three anonymous functions (each of which retains the value of i) are added to a List lstFun. When calling them (the call is made with the () operator after the list element lstFun[i]), they know their value of i; this is a great improvement over JavaScript.

main() {
  var lstFun = [];
  for(var i in [10, 20, 30]) {
    lstFun.add( () => print(i) );
  }
  print(lstFun[0]()); //  10  null
  print(lstFun[1]()); //  20  null
  print(lstFun[2]()); //  30  null
}

While all these code variations might now perhaps seem as just a esthetical, they can make your code clearer in more complex examples and we'll make good use of them in the forthcoming apps. The definition of a function comprises its name, parameters, and return type and is also called its signature. If you find this signature occurring often in your code, you can define it as a function type with typedef, as shown in the following code:

typedef int addInts(int a, b);

Then you can use addInts as the type of a function that takes two values of int and returns an int.

Both in functional and OO programming it is essential to break a large problem into smaller ones. In functional programming, the decomposition in functions is used to support a divide-and-conquer approach to problem solving. A last remark: Dart does not have overloading of functions (or methods or constructors) because typing the arguments is not a requirement, Dart can't make the distinction. Every function must have a unique name, and there can be only one constructor named after the class, but a class can have other constructors as well.