Difference between revisions of "An Overview of Kotlin Functions and Lambdas"
(Created page with "Kotlin functions and lambdas are a vital part of writing well-structured and efficient code and provide a way to organize programs while avoiding code repetition. In this chap...") |
(No difference)
|
Revision as of 20:51, 21 November 2017
Kotlin functions and lambdas are a vital part of writing well-structured and efficient code and provide a way to organize programs while avoiding code repetition. In this chapter we will look at how functions and lambdas are declared and used within Kotlin.
Contents | ||
What is a Function?
A function is a named block of code that can be called upon to perform a specific task. It can be provided data on which to perform the task and is capable of returning results to the code that called it. For example, if a particular arithmetic calculation needs to be performed in a Kotlin program, the code to perform the arithmetic can be placed in a function. The function can be programmed to accept the values on which the arithmetic is to be performed (referred to as parameters) and to return the result of the calculation. At any point in the program code where the calculation is required the function is simply called, parameter values passed through as arguments and the result returned.
The terms parameter and argument are often used interchangeably when discussing functions. There is, however, a subtle difference. The values that a function is able to accept when it is called are referred to as parameters. At the point that the function is actually called and passed those values, however, they are referred to as arguments.
How to Declare a Kotlin Function
A Kotlin function is declared using the following syntax:
fun <function name> (<para name>: <para type>, <para name>: <para type>, ... ): <return type> { // Function code }
This combination of function name, parameters and return type are referred to as the function signature or type. Explanations of the various fields of the function declaration are as follows:
fun – The prefix keyword used to notify the Kotlin compiler that this is a function. <function name> - The name assigned to the function. This is the name by which the function will be referenced when it is called from within the application code. <para name> - The name by which the parameter is to be referenced in the function code. <para type> - The type of the corresponding parameter. <return type> - The data type of the result returned by the function. If the function does not return a result then no return type is specified. Function code - The code of the function that does the work.
As an example, the following function takes no parameters, returns no result and simply displays a message:
fun sayHello() { println("Hello") }
The following sample function, on the other hand, takes an integer and a string as parameters and returns a string result:
fun buildMessageFor(name: String, count: Int): String { return("$name, you are customer number $count") }
Calling a Kotlin Function
Once declared, functions are called using the following syntax:
<function name> (<arg1>, <arg2>, ... )
Each argument passed through to a function must match the parameters the function is configured to accept. For example, to call a function named sayHello that takes no parameters and returns no value, we would write the following code:
sayHello()
In the case of a message that accepts parameters, the function could be called as follows:
buildMessageFor("John", 10)
Single Expression Functions
When a function contains a single expression, it is not necessary to include the braces around the expression. All that is required is an equals sign (=) after the function declaration followed by the expression. The following function contains a single expression declared in the usual way:
fun multiply(x: Int, y: Int): Int { return x * y }
Below is the same function expressed as a single line expression:
fun multiply(x: Int, y: Int): Int = x * y
When using single line expressions, the return type may be omitted in situations where the compiler is able to infer the type returned by the expression making for even more compact code:
fun multiply(x: Int, y: Int) = x * y
Local Functions
A local function is a function that is embedded within another function. In addition, a local function has access to all of the variables contained within the enclosing function:
fun main(args: Array<String>) { val name = "John" val count = 5 fun displayString() { for (index in 0..count) { println(name) } } displayString() }
Handling Return Values
To call a function named buildMessage that takes two parameters and returns a result, on the other hand, we might write the following code:
val message = buildMessageFor("John", 10)
To improve code readability, the parameter names may also be specified when making the function call:
val message = buildMessageFor(name = "John", count = 10)
In the above examples, we have created a new variable called message and then used the assignment operator (=) to store the result returned by the function.
Declaring Default Function Parameters
Kotlin provides the ability to designate a default parameter value to be used in the event that the value is not provided as an argument when the function is called. This simply involves assigning the default value to the parameter when the function is declared.
To see default parameters in action the buildMessageFor function will be modified so that the string “Customer” is used as a default in the event that a customer name is not passed through as an argument. Similarly, the count parameter is declared with a default value of 0:
fun buildMessageFor(name: String = "Customer", count: Int = 0): String { return("$name, you are customer number $count") }
When parameter names are used when making the function call, any parameters for which defaults have been specified may be omitted. The following function call, for example, omits the customer name argument but still compiles because the parameter name has been specified for the second argument:
val message = buildMessageFor(count = 10)
If parameter names are not used within the function call, however, only the trailing arguments may be omitted:
val message = buildMessageFor("John") // Valid val message = buildMessageFor(10) // Invalid
Variable Number of Function Parameters
It is not always possible to know in advance the number of parameters a function will need to accept when it is called within application code. Kotlin handles this possibility through the use of the vararg keyword to indicate that the function accepts an arbitrary number of parameters of a specified data type. Within the body of the function, the parameters are made available in the form of an array object. The following function, for example, takes as parameters a variable number of String values and then outputs them to the console panel:
fun displayStrings(vararg strings: String) { for (string in strings) { println(string) } } displayStrings("one", "two", "three", "four")
Kotlin does not permit multiple vararg parameters within a function and any single parameters supported by the function must be declared before the vararg declaration:
fun displayStrings(name: String, vararg strings: String) { for (string in strings) { println(string) } }
Lambda Expressions
Having covered the basics of functions in Kotlin it is now time to look at the concept of lambda expressions. Essentially, lambdas are self-contained blocks of code. The following code, for example, declares a lambda, assigns it to a variable named sayHello and then calls the function via the lambda reference:
val sayHello = { println("Hello") } sayHello()
Lambda expressions may also be configured to accept parameters and return results. The syntax for this is as follows:
{<para name>: <para type>, <para name> <para type>, ... -> // Lambda expression here }
The following lambda expression, for example, accepts two integer parameters and returns an integer result:
val multiply = {val1: Int, val2: Int -> val1 * val2 } val result = multiply(10, 20)
Note that the above lambda examples have assigned the lambda code block to a variable. This is also possible when working with functions. Of course, the following syntax will execute the function and assign the result of that execution to a variable, instead of assigning the function itself to the variable:
val myvar = myfunction()
To assign a function reference to a variable, simply remove the parentheses and prefix the function name with double colons (::) as follows. The function may then be called simply by referencing the variable name:
val myvar = ::myfunction myvar()
A lambda block may be executed directly by placing parentheses at the end of the expression including any arguments. The following lambda directly executes the multiplication lambda expression multiplying 10 by 20.
val result = {val1: Int, val2: Int -> val1 * val2 }(10, 20)
The last expression within a lambda serves as the expressions return value (hence the value of 200 being assigned to the result variable in the above multiplication examples). In fact, unlike functions, lambdas do not support the return statement. In the absence of an expression that returns a result (such as an arithmetic or comparison expression), simply declaring the value as the last item in the lambda will cause that value to be returned. The following lambda returns the Boolean true value after printing a message:
val result = { println("Hello"); true }()
Similarly, the following lambda simply returns a string literal:
val nextmessage = { println("Hello"); "Goodbye" }()
A particularly useful feature of lambdas and the ability to create function references is that they can be both passed to functions as arguments and returned as results. This concept, however, requires an understanding of function types and higher-order functions.
Higher-order Functions
On the surface, lambdas and function references do not seem to be particularly compelling features. The possibilities that these features offer become more apparent, however, when we consider that lambdas and function references have the capabilities of many other data types. In particular, these may be passed through as arguments to another function, or even returned as a result from a function.
A function that is capable of receiving a function or lambda as an argument, or returning one as a result is referred to as a higher-order function.
Before we look at what is, essentially, the ability to plug one function into another, it is first necessary to explore the concept of function types. The type of a function is dictated by a combination of the parameters it accepts and the type of result it returns. A function which accepts an Int and a Double as parameters and returns a String result for example is considered to have the following function type:
(Int, Double) -> String
In order to accept a function as a parameter, the receiving function simply declares the type of the function it is able to accept.
For the purposes of an example, we will begin by declaring two unit conversion functions:
fun inchesToFeet (inches: Double): Double { return inches * 0.0833333 } fun inchesToYards (inches: Double): Double { return inches * 0.0277778 }
The example now needs an additional function, the purpose of which is to perform a unit conversion and print the result in the console panel. This function needs to be as general purpose as possible, capable of performing a variety of different measurement unit conversions. In order to demonstrate functions as parameters, this new function will take as a parameter a function type that matches both the inchesToFeet and inchesToYards functions together with a value to be converted. Since the type of these functions is equivalent to (Double) -> Double, our general purpose function can be written as follows:
fun outputConversion(converterFunc: (Double) -> Double, value: Double) { val result = converterFunc(value) println("Result of conversion is $result") }
When the outputConversion function is called, it will need to be passed a function matching the declared type. That function will be called to perform the conversion and the result displayed in the console panel. This means that the same function can be called to convert inches to both feet and yards, simply by “plugging in” the appropriate converter function as a parameter, keeping in mind that it is the function reference that is being passed as an argument:
outputConversion(::inchesToFeet, 22.45) outputConversion(::inchesToYards, 22.45)
Functions can also be returned as a data type simply by declaring the type of the function as the return type. The following function is configured to return either our inchesToFeet or inchesToYards function type (in other words a function which accepts and returns a Double value) based on the value of a Boolean parameter:
fun decideFunction(feet: Boolean): (Double) -> Double { if (feet) { return ::inchesToFeet } else { return ::inchesToYards } }
When called, the function will return a function reference which can then be used to perform the conversion:
val converter = decideFunction(true) val result = converter(22.4) println(result)
Summary
Functions and lambda expressions are self-contained blocks of code that can be called upon to perform a specific task and provide a mechanism for structuring code and promoting reuse. This chapter has introduced the basic concepts of function and lambda declaration and implementation in addition to the use of higher-order functions that allow lambdas and functions to be passed as arguments and returned as results.