Using functions in Dart
Functions are another tool to change the program 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.
However, Dart is both functional and object-oriented. Functions are first-class objects themselves (they are of the function type) 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 (have a so-called side effect): the return type, if indicated, is
void
, for example, thedisplay
function inreturn_types.dart
. In fact, such a function does return an object, namelynull
(see the print in line(1)
of the following code). - Return an
exp
expression, resulting in an object different fromnull
, explicitly indicated byreturn exp
like indisplayStr
(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. The exp
is an expression that returns a value, but it cannot be a statement like if
. A function can be an argument to another function, like display
in print
line (1)
, or in line (4)
, where the isOdd
function is passed to the where
function:
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 becomes more general; its argument can be a String, number, Boolean, List, and so on.
Parameters
As all the parameter variables are objects, all the parameters are passed by reference; this 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 the parameters we have seen so far in the examples were required, but the 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 an argument, the optional value will be substituted instead, but when called with an argument, this will take 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 this 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
While 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
To summarized it:
- 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, such 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 of this outer function. A slight variation would be to store the function in a calc
variable that has the Function
type, like 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 the years
parameter 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 calc
function is made top-level and is passed in the lineOut
function 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"); }
In the variation to the previous code, prorabbits_v7.dart
has the calc
inner function that has no parameter, yet it can use the years
variable that exists in the surrounding scope. For this 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 shown by closure.dart
. The multiply
function returns a function (that itself takes an i
parameter). 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 calledpartial 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 lstFun
list. While calling them (the call is made with the ()
operator after the lstFun[i]
list element), 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 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 of its name, parameters, and return type, which 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
value.
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. As 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.