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, 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 expression
exp
resulting in an object different fromnull
, explicitly indicated by areturn exp
, as 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. 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 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 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.