Chapter 4. Functional first-class functions and closures

This chapter covers

  • Declaring functions and return types
  • Specifying mandatory and optional parameters
  • Storing first-class functions in variables
  • Using functions as closures

Dart is similar in many ways to languages such as Java and C#, but its function syntax is more similar to that found in JavaScript than in more strongly typed languages. In Dart, everything is an object, including functions, which means you can store a function in a variable and pass it around your application the same way that you might pass a String, an int, or any other object. This is known as having first-class functions, because they’re treated as equivalent to other types and aren’t second-class citizens in the language.

First, we’ll examine Dart’s function syntax options, which have a longhand and a shorthand notation and various forms, depending on how much optional type information you provide. You’ll take these functions and use them as variables and pass functions around as parameters and return types.

Once you have a grounding in using functions as first-class objects by storing them in variables, we’ll look at how you can define function types to provide strong type information. These definitions help the type system validate that functions you’re passing around your application match the developers’ intentions.

Finally, we’ll look at closures, which occur when a function is created and uses variables that exist outside of its own scope. When you pass that function (stored in a variable) to another part of the application, it’s known as a closure. This can be a complex topic; it’s used extensively in JavaScript to emulate features such as getters and setters, and class privacy—features that are already built into the Dart language.

 

Note

In general, everything you’ll discover about functions is also applicable to methods, which are a special type of function that’s associated with a class. We’ll look in detail at classes in chapter 6.

 

4.1. Examining Dart functions

A computer program is a list of instructions or steps that the computer must execute in a certain order, sometimes using the output of one step as the input to another step. In the real world, you perform functions with inputs and outputs all the time. For example, when mixing concrete for a garden project (I often find myself doing more garden construction than tending to actual plants), I follow this recipe to make great general-purpose concrete. Each function takes inputs and outputs, and the functions I perform are highlighted in bold:

1.  Measure the quantity of cement (the cement volume).

2.  Measure the quantity of sand as twice the cement volume.

3.  Measure the quantity of gravel as three times the cement volume.

4.  Mix the cement and sand to create a mortar mix.

5.  Mix the mortar mix with the gravel to create a dry concrete mix.

6.  Mix the concrete mix with water to create wet concrete.

7.  Lay the concrete before it sets.

The measure() and mix() functions are reused throughout these steps, taking the input of a previous step to produce a new output. When I mix two ingredients, such as cement and sand, this gives me a new ingredient (mortar) that I can use elsewhere in the recipe. There is also a lay() function, which I use only once. The initial volume of the starting quantity of cement depends on the job; for example purposes, I use a bag as an approximate unit of measure.

You can represent these functions in Dart using the code in listing 4.1. The listing omits the various Ingredient classes that the functions return, but for this example, they’re unnecessary (you can find them in the source code associated with this book). The set of concrete-mixing instructions is followed in the main() function, which is the first function to execute in all Dart applications.

Listing 4.1. Mixing concrete in Dart

Dart functions are similar in declaration to Java and C#, in that they have a return type, a name, and a list of parameters. Unlike JavaScript, they don’t require the keyword function to declare that they’re functions; unlike Java and C#, the parameter types and return types are all optional, as part of Dart’s optional type system.

Now that some example functions are defined, let’s look at some other ways these functions could be defined in Dart, taking into account Dart’s optional typing and longhand and shorthand function syntax. Chapter 3 briefly examined Dart’s longhand and shorthand function syntax: the shorthand syntax allows you to write a single-line function that automatically returns the output of the single line. Dart’s optional typing also means the return type and the parameter types are both optional. Figure 4.1 shows various combinations of type information for the longhand and shorthand versions of the mix functions.

Figure 4.1. Longhand and shorthand versions of the mix functions in Dart

You can use Dart’s shorthand function syntax only for single-line functions, whereas you can use the longhand function syntax for single or multiline functions. The shorthand syntax is useful for writing concise, clear code. And as you’ll see in the following section, shorthand functions automatically return the result of the single line.

4.1.1. Function return types and the return keyword

All Dart functions return a value. For single-line shorthand functions, this value is always the result of the single-line expression. For longhand functions, the return value defaults to null unless you actively return a value.

Single-line shorthand functions automatically return the result of the single-line expression; for example, the shorthand mix(item1, item2) function returns the result of item1 + item2 without explicitly stating that the function will return a value. The shorthand syntax, which uses the form

function_name() => expression;

can be read as “return the result of the expression.” This is always the case for shorthand syntax functions, even if the expression doesn’t obviously produce a value, such as calling another function with no return value. Because the default return value is null, you always end up with a null value returned in the absence of any other value.

Longhand functions, on the other hand, always return the default null if you don’t actively return another value by using the return keyword. The return keyword can also specify an optional value to return (which replaces the null default), such as in this longhand mix() function:

return item1 + item2;

When return is used, it immediately returns the result to the calling code. If a longhand function does explicitly return a value with the return keyword, then the function returns null. Figure 4.2 shows the return keyword used in a longhand function and the expression that’s evaluated for a return value in the shorthand style. It also shows a common error that can occur in your code: forgetting to use the return keyword.

Figure 4.2. Longhand functions require the return keyword to return a value, whereas shorthand functions return the result of their single-line expression automatically.

Specifying a Return Type

You can specify the return type of the function by prefixing the function name with the type you’re expecting to return. Dart’s optional typing means if you don’t explicitly specify a return type, then you’re returning a dynamic type, as in

When you specify a return type, the tools can validate code in three ways: first by providing a warning if you haven’t explicitly returned a value in longhand functions (using the return keyword), and second by validating that the type you’re returning is the type that was expected to be returned—for example, that you’re indeed returning an Ingredient object. The third validation method is provided to users of your function. Because Dart’s type system provides documentation, the explicit return type is used to validate that the calling code is correctly handling the return type. For example, you might try to assign the result of mix(), which is an Ingredient, into a ConcreteMix type variable, which you might then try to lay. Without a return type specified, this code would be valid but incorrect. But when you document your function with the proper Ingredient return type, the tools can alert you to your error:

The mix() function explicitly returns an Ingredient type, but the lay() function explicitly expects a ConcreteMix type. Without the explicit type information, Dart would allow this code to run (until the code failed later in another flow).

Using the Void Type to Indicate no Return Value

This behavior raises a new problem: how do you explicitly state that you aren’t expecting to return a value from a function? All Dart functions return a value, but you can provide documentation to the type checker with the void type to indicate that you aren’t expecting to return a value.

Imagine that you’re going to use a small cement mixer to do the mixing for you. You can create a function to start the mixer that returns no value:

startMixer(speed) {
  Mixer.start(speed);
}

Because all Dart functions return a value, this code automatically returns a default null value to the calling code. Users of the startMixer() function as it’s currently declared can’t tell whether the designer of that function intended a null value to be returned or left out the return keyword in front of the call to Mixer.start() (which might be a coding bug). When you try to store the return value of startMixer, it contains null:

Fortunately, when you use the explicit return type void, you provide documentation to the user that you didn’t intend to return a value. Doing so also provides the type checker with information that it can use to provide warnings if you try to use the return value. It doesn’t stop the function from returning the default null value, but it does warn you that you shouldn’t be using that returned value. You can modify the function as follows:

Providing void as a return type also validates that the function code doesn’t use the return keyword to return a value.

4.1.2. Providing input with function parameters

In addition to returning values, Dart functions take parameters as inputs. Unlike in JavaScript, you must specify all the parameter names in a function definition. In JavaScript the parameter list is optional, because all the arguments passed to a function can be accessed dynamically using an internal arguments variable. Dart is much more structured, requiring all the parameters to be defined in the function definition in a manner similar to that of Java or C#. Dart doesn’t have function (or method) overloading, but it does have optional parameters.

 

Tip

There is no difference in the parameter definitions for longhand and shorthand functions, so the examples show only the longhand syntax in this section, but they apply equally to either syntax.

 

The measureQty(ingredient, numberOfCementBags, proportion) function currently takes three parameters: ingredient, numberOfCementBags, and proportion. The function can use them in the same way it would use variables in the scope of the function. The typed and untyped versions of the function signature look like the following:

When you provide type information such as Ingredient and int for function parameters, you’re declaring that the input types should have an is-an relationship with Ingredient and int, respectively. This allows the type checker to validate that the calling code is passing correctly typed arguments to the measureQty() function. Calling code must pass all the arguments in the same order in which they appear in the function declaration; in the example, this means ingredient must come first, then numberOfCementBags, and finally proportion:

var sand = measureQty(new Sand(), cement.bags, 2);
Arguments are Passed by Reference

When you call the measureQty() function to pass in arguments, each argument—for example, a sand ingredient—contains a reference to the value passed in. Therefore, you can change the properties of the ingredient (such as the quantity remaining) but not the ingredient itself, as shown in figure 4.3.

Figure 4.3. When you pass an object by reference, you can change the properties of that object but not the object itself.

For example, what happens if you create a new bag of sand in the measureQty() function, realize that the bag passed in doesn’t contain enough, and grab some more from the store? It turns out that in this case, the original bag of sand remains untouched. The following listing demonstrates this concept in action.

Listing 4.2. Checking parameters passed by reference

This code works the same regardless of whether you’re modifying an object, such as the ingredient instance, or a more primitive type, such as an int. You lose the reference to the new type on return because everything is an object (as discussed in the previous chapter); when you change the reference to the object being passed in, all you’re doing in the function is losing the original reference and creating a new reference. The code outside the function still has a handle on the original object reference.

Optional Positional Parameters

Dart functions can have optional parameters with default values. When you’re creating a function, you can specify parameters that calling code can provide; but if the calling code chooses not to, the function uses the default values.

When measuring out the quantity of ingredients, which are proportional to the number of bags of cement, you call the measureQty() function, which returns a new ingredient based on the number of bags of cement and the required proportion. Sometimes you want a 1:1 ratio between the number of bags of cement and your input ingredient. Without using optional parameters, you can modify the function to check if numberOfCementBags and proportion are null and then default them to 1 as follows:

The calling code needs to know the number of bags and proportion values to pass into the measureQty() function. This is true even if the calling code wants to pass in a standard, default value, such as a proportion of 1. Calling code can make calls like this:

measureQty(new Sand(), null, null);
measureQty(new Sand(), 1, null);
measureQty(new Sand(), null, 1);
measureQty(new Sand(), 1,1);

These are known as positional arguments; their position in the calling code matters. For example, the third argument is the proportion, and the first is the ingredient.

It would be better if the calling code could pass in only the values it needed to, such as the ingredient and the proportion, without passing in the number of bags if it wasn’t required. Dart lets us achieve this with optional parameters. Optional parameters must appear in a block together after all positional parameters are defined. The optional parameters block is defined within a pair of square brackets and, like positional parameters, is a comma-separated list. For example, you can change the example function to support optional parameters (shown in bold) as follows:

measureQty(ingredient, [int numberOfCementBags, int proportion]) {
  // ... snip ...
}

Now calling code can provide values for numberOfCementBags and proportion only if it needs to.

You can refer to optional parameters by position, by providing arguments for them in the order in which they’re declared:

Of course, in this code the parameter values will still be initialized to null if they aren’t provided, which means measureQty() still has to check for null values and default them to 1. Fortunately, you can also provide default values as part of the named parameter’s function declaration:

Now calling code can opt to either provide the parameters or not, and your function is simplified by not requiring a null check on the parameters. As noted earlier, the mandatory parameters must all be passed in, and in the correct order. But this isn’t true for optional parameters. Calling code can use optional parameters in one of two ways:

  • Not provide values at all. The function uses the default value or null if no default value is specified.
  • Provide a value for each optional parameter in the order in which they’re declared, reading from left to right. The calling code can provide arguments to populate each of the optional parameters. Any parameters not populated default to null or the specified default value.
Optional Named Parameters

An alternative to optional positional parameters is to use optional named parameters. These allow calling code to specify the parameters into which it’s passing values, in any order. As before, mandatory parameters come first, but this time the optional parameters are specified between curly braces, with default values provided in this form:

Note that unlike the optional positional parameters, the default values for optional named parameters use a colon (:) to separate the parameter name and value. Calling code can now call the measureQty() function, passing in the mandatory ingredient argument and optionally the numberOfCementBags and proportion arguments, in any order:

Unlike optional positional parameters, calling code must specify the parameter name for all supplied optional named parameter values. This means the following function call isn’t valid

measureQty(new Sand(), 2, 1);

because the optional values must be named.

 

Note

With mandatory positional parameters, calling code has no knowledge about the parameter names. Optional named parameters, on the other hand, form part of your code’s API, so you should spend as much time naming optional parameters as you’d spend on other API names, such as function and method names. Changing the name of an optional parameter once you’ve released your code could affect other users of your code in the same way as changing a function name.

 

You can’t define both optional named parameters and optional positional parameters in the same function definition. You should make a choice based on the likely use cases for your function.

Figure 4.4 shows different ways to call the measureQty() function.

Figure 4.4. The different ways that calling code can supply values for optional positional and named function parameters

 

Remember

  • Shorthand functions automatically return the value created by the single-line expression that forms the function body.
  • Longhand functions should use the return keyword to return a value; otherwise, null is automatically returned.
  • You can tell the type checker that you aren’t intending to return a value by using the return type void.
  • Type information on parameters is optional.
  • You can declare optional parameters as a comma-separated list within square brackets after the mandatory parameters are declared.
  • Calling code can refer to optional parameters by name, using a name:value syntax.

 

Now that you know what a function looks like, how to call it, how to specify return types, and how to use and mandatory and optional parameters, it’s time to look at what else you can do with functions: specifically, how to store functions in a variable and pass them into other functions as function arguments.

4.2. Using first-class functions

The term first-class functions means you can store a function in a variable and pass it around your application. There’s no special syntax for first-class functions, and all functions in Dart are first class. To access the function object (rather than call the function), refer to the function name without the parentheses that you’d normally use to supply parameters to the function. When you do this, you have access to the function object.

Consider the Ingredient mix(item1, item2) function from earlier in the chapter. You can call it by following the function name with parentheses and passing values for the function arguments, such as mix(sand,cement);. You can also refer to it just by name, without the parentheses and parameters; this way you get a reference to the function object that you can use just like any other value, such as a String or an int. Once you have the function object stored in a variable, you use that new reference to call the function again, as shown in the following snippet:

You can see from the example that the mix() function (and the mixFunction variable) has an is-an relationship with both the Object type (remember, everything is-an object), and it also has an is-an relationship with a class called Function, which represents the function type.

This concept raises an interesting possibility. If you can store a function in a variable, do you need to declare the function in the top-level scope first? No, you can declare a function inline (within another function body) and store it in a variable, rather than declare a function in the top-level library scope. In fact, there are three ways to declare a function inline and one way to do so in the top-level library scope, as shown in figure 4.5; the function declarations are highlighted in bold. We’ll go through each of these examples in the next few pages.

Figure 4.5. The four different ways to declare a function. mix1() is in the top-level scope, and the other three are declared in a function body.

 

Function scope vs. library scope

The largest block of scope in Dart is the Library, and all functions that aren’t wrapped inside another block, such as another function or a class definition (where they’re called methods), exist in library scope. These are considered to be at the top level of the Library.

You can also declare functions inside another function. These are considered to be in function scope like any other variable declared in a function, such as a String or an int. You can access these function-scoped functions only in the block where they were declared, unless you pass them to another function as a parameter or return value (just like other variables). You’ll see examples of this when we discuss closures later in the chapter.

 

You’ve already used the top-level library scope to declare functions such as mix1(), which is known as a library function. The other three function declarations, all within the body of another method, are known as local functions, which need more explanation. They’re a part of Dart that looks deceptively simple but, like closures, can be complex.

4.2.1. Local function declarations

Local functions are functions that you declare within the body of another function. Unlike library functions, you can’t explicitly reference them outside of the function in which they were declared. But you can pass them into other functions and use them as return values from the function you declared them in and store them in a list so that some code block can dynamically execute them in turn.

Listing 4.3 defines and uses the combineIngredients() function, which takes a mixing function and a pair of ingredients as its parameters. By accepting an arbitrary mixing function as a parameter, the combineIngredients() function allows you to mix ingredients with any implementation of that function you want, such as mixing with a shovel or mixing with a cement mixer. I’ll refer to combineIngredients() throughout this section.

Listing 4.3. Function that takes a function object as a parameter

Now that you have a use for a function object stored in a variable, let’s look at the three ways of declaring local functions, starting with the most basic: the simple local function declaration. In the following sections, the examples all use the longhand syntax, but the rules apply equally to the shorthand function syntax.

Simple Local Function Declaration

The simple local function declaration shown in figure 4.6 shares the same syntax as library functions declared in the top-level scope, except that they happen to exist within another function. Their name is also their reference, as with mix2(); and their own name exists in their own scope, so they can be recursive (I’ll discuss recursive functions a little later in this chapter).

Figure 4.6. Simple local function declaration syntax

When you’re declaring a simple local function in the scope of another function, you don’t need to provide a terminating semicolon, because the closing brace provides the terminator—it’s the same as declaring a function in the top-level scope. This is an important point, because the other two methods of declaration—which explicitly assign a function to a variable—do require a semicolon following the closing brace.

You can strongly type a function by providing type information in the same manner as top-level, library-scoped functions. The following listing defines a strongly typed function mix2() within the main() function, which is passed into the combineIngredients() function.

Listing 4.4. Outer main() function uses the inner mix() function

The method name mix2() exists in the scope of the mix2() function itself. This setup allows mix2() to call itself (creating recursion) or refer to its own function object.

 

Recursion

A recursive function is a function that calls itself. It’s a technique often used in sort and search functions to traverse trees of data. Consider the following recursive function called stir(), which calls itself while the stir count is less than 10:

There are a number of good resources on the internet about recursion as a computer science topic—it isn’t a technique found only in Dart. What’s important is that for this technique to work, the name of the function must be available in its own scope; that is, the method stir() must exist and must be valid to refer to it when it’s called. Simple local function declarations, in common with top-level, library-scoped function declarations, have their own name in scope and can therefore recurse.

 

Anonymous Function Declaration

An anonymous function is declared without a function name; see figure 4.7. Like any other function declaration, you can assign it to a function object variable, pass it directly into another function, or use it as the return type of the declaring function. But you can’t use it recursively, because the function has no name for itself in its own scope.

Figure 4.7. Anonymous function declaration

The longhand version of this function requires a terminating semicolon because you’re assigning a value to a variable (mix3), and that variable assignment statement needs to be terminated before the code can move onto the next statement.

An anonymous function starts with the opening parenthesis and continues to the end of the function body. With function shorthand, you can declare an anonymous function in the following form:

() => null;

This function, declared as is, can never be called, because it has no name to reference it by. But this is a valid anonymous function that takes no parameters and returns a null value.

Anonymous functions are often used to pass a function directly into another function as a parameter or store functions in a list. These two methods work because the reference to the function object is preserved by either the parameter name of the receiving function or the element in the list. Listing 4.5 shows this concept in action by creating a list of anonymous functions for processing extra ingredients. You then call each function in the list in turn. The anonymous functions are highlighted in bold, and longhand and shorthand versions are shown. The anonymous functions are passed as parameters into the List.add() method, where they can be referred to later (just like a String, an int, or any other object).

Listing 4.5. Storing anonymous functions in a list

This pattern is used extensively with the browser event handlers. You can add multiple functions to specific events, such as button.on.click.add(), which takes a function as its parameter and adds that function to a list of handlers that are called when the button is clicked. You can still use this pattern by passing in a reference to a function by name, but often—especially for single-line functions—it’s simpler and more readable to pass the function in anonymously, as in the following call to combineIngredients, which takes a function as a parameter. The anonymous function is in bold:

combineIngredients( (item1, item2) => item1 + item2, sand, gravel);

You’re still able to provide type information for the parameters in the parameter list but not for the return type, because Dart thinks any text to the left of the opening parenthesis is the function name (thus making it a named function rather than an anonymous function). The following would be a function called Ingredient, rather than an anonymous function that returns an ingredient:

This issue can be resolved with the third and final way of declaring local functions: function assignment.

Named Function Assignment Declaration

The third way of declaring a function is a hybrid of both of the previous versions in that it declares a named function and immediately assigns that function to a variable. Because this is an assignment, like the previous example, you must also terminate this assignment with a semicolon, as shown in figure 4.8.

Figure 4.8. Named function assignment declaration

This approach has the advantages that you can declare the return type and you have a function name that’s in the scope of the function, allowing recursion if required. In this example, the function name in the scope of the function is mixer(), and this name is available only in the scope of the function. To refer to the function elsewhere, you must use the name mix4.

You can rewrite the mix4() function to use recursion and provide type information if you pass it as an argument to the combineIngredients() function, as shown next.

Listing 4.6. Recursive, typed, named function

The name mixer() is essentially a throwaway. It’s available only in the scope of the function and isn’t valid elsewhere. When you declare the mixer() function directly into another function as an argument, you can’t refer to mixer() anywhere but within itself. This example looks nearly identical to the simple, local function declaration that we looked at first but is subtly different by virtue of the function being assigned implicitly to the parameter of the combineIngredients() function, as shown in figure 4.9.

Figure 4.9. A named function declared as an argument to another function can refer to itself by name. That name isn’t available for use elsewhere.

We’ve looked at declaring functions and assigning them to variables and function parameters, but what about Dart’s type system? How do you know that the combineIngredients() function takes another function as its first parameter? Fortunately, Dart allows strong function typing and provides a new keyword, typedef, which I’ll discuss next.

4.2.2. Defining strong function types

So far, you’ve been storing your function objects in dynamically typed variables and passing the functions’ objects into other functions as dynamic parameters. This approach presents a problem in that Dart’s optional typing allows you to specify the type of a variable or a function parameter: what happens when you want to specify that the type is a function? You’ve already seen that a function “is-an” Object and a function “is-a” Function, so you can use these types as shown in the following listing.

Listing 4.7. Function type strongly types a function variable or parameter

When you use a function object stored in a variable, you’re using an instance of a Function class. Not all function instances are the same, however. The mix() function is different from the measureQty() function, which is different from the lay() function. You need a way to strongly type the mix() function parameter on combineIngredients() to specify that it wants a mix() function and not one of the others.

Dart provides two ways to achieve this. The first is lighter weight but slightly more verbose: provide the function signature as the function parameter definition, as shown in figure 4.10.

Figure 4.10. The parameters of a function can be defined to accept another function with a specific signature.

This approach is a verbose way of declaring that a function parameter must have a specific signature. Imagine if you had 10 functions that all accepted a mix() function; you’d need to write the function 10 times. Fortunately, Dart allows you to declare a function signature by using the typedef keyword, which lets you create a custom function type. typedef declares that you’re defining a function signature, not a function or a function object. You can use typedef only in the library’s top-level scope, not within another function. The following listing shows how you can use typedef to define a function signature that you can use to replace the mixFunc parameter declaration on the combineIngredients() parameter list.

Listing 4.8. Using typedef to declare a function signature

With typedef, you can create shorthand for a specific function signature that you can use in variable and parameter definitions, which can let the type checker validate that the correct function objects are being passed around your application.

Now that you’re familiar with the different ways to declare functions in the local scope of another function, it’s time to tackle closures: when a function object refers to another variable that was declared outside of its own immediate scope. Closures are a powerful functional programming concept.

 

Remember

  • When you use a function by name, without the parameter brackets, you get a reference to its function object.
  • Simple local functions declared in a similar manner to top-level, library-scoped functions are able to refer to themselves by name and can make full use of parameter and return type information to provide type information to the tools.
  • Anonymous functions have no name and can’t use recursion or specify strong return type information, but they do provide useful shorthand for adding functions into lists or passing to other functions as parameters.
  • You can use a named function in place of an anonymous function to allow recursion and strong return type information, but its name is available only in the scope of itself.
  • You can use the typedef keyword to declare a specific function signature so the type checker can validate function objects.

 

4.3. Closures

Closures are a special way of using functions. Developers often create them without realizing it when passing a function object around an application. Closures are used extensively in JavaScript to emulate various constructs found in class-based languages, such as getters, setters, and private properties, by creating functions whose sole purpose is to return another function. But Dart supports these constructs natively; you’re therefore unlikely to need closures for this purpose when writing new code. A large amount of code is likely to be ported from JavaScript to Dart, though, and Dart’s closure support is similar to JavaScript, which will aid this effort.

When you declare a function, it isn’t executed immediately; it’s stored in a variable as a function object in the same way that you might store a String or an int in a variable. Likewise, when you declare a function, you can also use other variables that you’ve declared before it, as in the following snippet:

This code lets you create a mix() function that always mixes two ingredients with cement. Instead of passing in cement as a separate ingredient every time, you declare cement first and then use cement from within the function.

When you call the mix() function, passing in sand and gravel, you still have access to the cement variable that was declared outside the mix() function’s scope. You can pass this function back to your combineIngredients() function, where it will happily mix the other two ingredients with the cement without ever knowing cement was involved. Figure 4.11 shows this happening.

Figure 4.11. The mix() function retains a reference to the cement variable even when mix() is passed out of the scope of the main() function that declared it.

This design is essentially a closure; it’s a function that retains a reference to variables that were in its scope when it was declared.

 

Why “closure”?

The term closure derives from close-over, which is one way of thinking about how a closure works. It “closes over” or wraps any nonlocal variables that were valid in its scope at the time it was declared.

 

As you just saw with the cement example, one of the times a closure is useful is when you want to provide some implementation details while keeping them hidden from the function that’s using that closure.

Closures are also formed when one function is returned by another function. You might have a getShovel() function that returns a shovel. You can use the shovel as a function to mix your ingredients, but—as shown in listing 4.9—the shovel also has some sticky mud on it. When the getShovel() function returns, the mix() function retains a reference to stickyMud, which is mixed with your ingredients even though the getShovel() function has exited.

Listing 4.9. Creating a closure with a function as a return type

Closures can occur accidentally because it’s perfectly valid to use variables that a function sees in its own scope and the scope of its parent. When you pass the child function out of its parent, either as a return type or as a function parameter, you’ll find that you’re working with a closure.

 

Remember

  • A function that uses variables that weren’t declared in its own scope has the potential to become a closure.
  • A function becomes a closure when that function is passed out of the scope from which it was declared, by either passing it into another function or returning from the function that declared it.

 

4.4. Summary

This chapter showed you how to declare functions using both shorthand and longhand syntax. When you use shorthand syntax, it also implicitly returns the value of the single-line expression that forms the shorthand function body. But when using longhand syntax, you must explicitly use the return keyword to return the value of an expression.

All functions return a value—null if no other value is specified—but you can tell the Dart tools that you aren’t expecting to specify a return value by using the void return type.

Functions can be stored in a variable or referenced by accessing them by name without the parentheses. This approach gives you a variable containing a function object, which you can pass around your app like any other variable. You can return a function object stored in a variable or pass it into another function, where it can be called like any other declared function. Function objects share an “is-an” relationship with the Function class.

To strongly type a function object variable or parameter so the type checker can validate your code, use the keyword typedef in the library’s top-level scope to define a named function signature. You can then use the name of the function signature the same way you would any other type.

We also looked at closures, which are created when a function uses variables that weren’t declared within that function, and that function is passed to another part of the code. You can use closures to use implementation details that the receiving function shouldn’t or can’t know about.

Now that you know all about functions, in the next chapter we’ll look at Dart’s library and privacy mechanisms. This information is important because the names of functions and classes that you’ll use in libraries have a strong bearing on privacy. The two concepts are closely linked, and it’s a topic that you need to understand before you start to look at Dart’s classes and interfaces.

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

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