Functions in Kotlin

Learn via video courses
Topics Covered

Overview

In Kotlin, functions consist of code blocks designed to accomplish particular tasks. They encapsulate a sequence of commands that can be invoked and executed repetitively within our program. By employing functions, we can enhance the organization and modularity of our code, contributing to improved readability, comprehension, and upkeep. In Kotlin, functions can take in parameters (inputs) and produce results (outputs), enabling the creation of reusable blocks of logic adaptable to various situations

Introduction

In order to use functions in kotlin it's essential to understand how to define and call them first.

How to create a user-defined function in Kotlin?

Before a function is called we need to make sure it's defined first.

Here's how we define a function in Kotlin:

Let's sum up the parts of a defined function

  1. fun: a keyword used to define a function in kotlin
  2. Identifier: name given to the function also pertaining to the naming conventions of kotlin programming language
  3. Parentheses (): This includes the possible parameters the function can possibly take
  4. Curly Braces {}: These braces enclose the contents or main body of the said function

We'll see an example for this further in this article

How to call a function?

We have to call the function to run codes inside the body of the function. Here's how:

This statement calls the callfunction() function declared earlier.

calling a function

Standard Library Function

Kotlin includes a variety of predefined functions within its standard library, ready for utilization. By supplying appropriate arguments, these functions can be invoked as needed.

Let's take a look at the list of different standard library functions and their use –

FunctionUse
sqrt()Calculates the square root of a number.
print()Prints a message to standard output.
rem()Finds the remainder of one number when divided by another
toInt()Converts a number to integer value.
readline()For standard input
compareTo()Compares two numbers and return boolean value.

Code

Output:

User-defined Function

Type of functions we can create ourself are called user-defined function. As opposed to standard library functions, these user-defined functions can be created at the whims of the user with different parameters, return value, name etc.

Simple Function

Here's a basic function that neither takes nor returns anything; it just prints a statement.

Code:

Output:

Parametrized Function

Parameters are values that we pass into a function to be used within the function's body.

It allows various data types like custom classes, primitives, and complex structures for performing operations within the function.

Single Parameter

A single parameter function takes only one parameter.

Multiple Parameters

A function is capable of taking multiple parameters as well

1. Parameters of same type

Output:

Let's examine the connection between a function and its parameters in this context.

In this scenario, when we call the addNumbers() function, we provide two Double-type arguments, number1 and number2. These provided arguments are referred to as the actual parameters.

The parameters n1 and n2 receive the supplied arguments within the function's definition. These supplied arguments are known as formal parameters (or simply parameters).

formal parameters

In Kotlin, arguments are distinguished by commas, and it's essential to explicitly specify the data type for formal arguments.

Keep in mind that the data types of actual and formal arguments must align. In other words, the data type of the first actual argument should correspond to the data type of the first formal argument, and the same principle applies to subsequent arguments.

2. Parameters of different type A function in kotlin can also have multiple parameters of different type. Let's look at an example.

Code:

Output:

Explanation: Here as we can see, printPersonInfo is declared with three parameters:

  • name, which is of type String.
  • age, which is of type Int.
  • isStudent, which is of type Boolean.

These parameters represent the information we want to print about a person

In the main function, when printPersonInfo is called say for the first time we have,

The first call provides Alice as the name (String), 25 as the age Int), and false as isStudent (Boolean).

Note how here as well the actual and formal parameters type match.

Functions with a return value

Let's follow our previous code

Here return sumInteger concludes the addNumbers() function. It's a signal that the work inside that function is done, and now, it's time for the program to switch back to the main() function.

So, in our program, the addNumbers() function yields the value sumInteger, which is then captured and stored in the variable named result.

It's kind of like getting a gift and putting it in a special place to use later!

addnumber function

Just like how actual and formal parameters' type needs to match, the same concept can be applied to the return value as well.

From the above example we can observe the following,

  1. sumInteger and result are of type Int
  2. return type of the function needs to be specified in the function definition

matches

Function usage

Default vs named arguments

Function parameters may be assigned default values, which come into play when we choose not to provide a value for the corresponding argument. This approach lessens the need for multiple function overloads.

To set a default value, we just append an = sign following the parameter's type.

During method overriding, the default parameter values of the base method are automatically passed down. When overriding a method that has default parameter values, it's important to exclude those default values from the method's signature.

When a default parameter is positioned before a parameter without a default value, we can only utilize the default value by invoking the function with named arguments.

When the final argument, following default parameters, is a lambda expression, we have the flexibility to provide it either as a named argument or outside the parentheses.

Named Arguments

When calling a function, naming its arguments is useful for managing complex functions, changing their order, or skipping some to use defaults.

Take, for example, the function reformat(), which boasts four parameters, each with default values.

When calling this function, we don't have to name all its arguments:

You can skip specific arguments with default values, but after skipping the first one, you must use named notation for the rest.

we can pass a variable number of arguments (vararg) with names using the spread operator:

In Java, you can't use named argument syntax when calling functions on the JVM because Java bytecode doesn't consistently store function parameter names.

Unit-returning Functions

In Kotlin, when a function doesn't return a value, it uses a return type called Unit. If a function has this return type, you don't need to explicitly specify it in the function definition; it's an implied convention.

In the greet function, notice that we didn't explicitly specify a return type. Since the function doesn't return any specific value (prints a greeting), Kotlin infers the return type as Unit.

Single Expression Functions

When the function body consists of a single expression, the curly braces can be omitted and the body specified after an = symbol:

Explicitly declaring the return type is optional when this can be inferred by the compiler:

Explicit Return Types

Single Return Value

Let's take another function example. This function will accept arguments and also returns a single value.

The addNumbers function takes two integer parameters, a and b, and returns their sum, which is of type Int. we can see the return type specified after the colon : Int

Multiple Return Value

In Kotlin, we can't return multiple values from a function in the traditional sense.

Then what do we do when we have to return multiple values from a function?

We use data classes, tuples, or other data structures to bundle multiple values together into a single return value.

Let's take an example of the most common approach for it which is to use a data class to represent a composite return value.

Code:

Output:

Explanation: We use a Result data class to bundle the sum and difference of two numbers calculated by the performMathOperations function, allowing us to return multiple values in a structured way.

Now what if the number of parameters of a function is more than just 2-3? what if it's more like 100? 1000? or what if we don't know if it's a fixed number at all even?

Variable number of arguments

In such a case parameter of a function can be marked with a vararg modifier. Let's see an example

Code:

Explanation: In Kotlin, a vararg parameter of type T is treated as an Array<out T>. For instance, you can call the asList() function like this: val list = asList(1, 2, 3). It's important to note that in Kotlin, only one parameter can be a vararg. If it's not the last parameter, you can use named arguments or pass a lambda outside the parentheses. To pass an array's contents to the function, use the spread operator *

Let's see an example,

Code:

Output:

Infix notation

Functions designated with the infix keyword can be invoked using infix notation, which means we can call them without the dot and parentheses. For this to work, infix functions must satisfy these conditions:

  • They should be either member functions or extension functions.
  • They must accept a single parameter.
  • The parameter must not support a variable number of arguments and should not have a default value.

In Kotlin, an infix function, like the one shown below:

Can be invoked using a more concise infix notation, like this:

Which is equivalent to the more traditional function call:

In Kotlin, infix function calls have a specific precedence level, which is lower than arithmetic operations, type casts, and the rangeTo operator. This makes expressions like these equivalent.

In Kotlin, infix function calls have higher precedence than boolean operators (&& and ||), is-checks, in-checks, and some other operators. This results in equivalences like these.

In Kotlin, remember that infix functions need both the receiver and the parameter to be explicitly stated. When using infix notation to call a method on the current receiver, always include this explicitly to ensure clear parsing.

Code:

Function scope

Scope Functions

In Kotlin's standard library, there are Scope Functions that allow you to execute a code block within an object's context. When you use these functions with a lambda expression on an object, they create a temporary scope, allowing you to work with the object without explicitly referencing its name.

FunctionObject ReferenceReturn ValueUse Case
letitLambda resultThe safe call operator ?. with let is employed for null safety to ensure that block execution occurs solely with non-null values. It's used to facilitate null safety calls.
runthisLambda resultEmployed when the object lambda encompasses both initialization and return value computation. By using run, we can carry out null safety calls and additional computations as needed.
withthisLambda resultIt is advisable to use with for invoking functions on context objects when we're not interested in the lambda's result..
applythisContext objectIt can be employed to manipulate the attributes of the receiver object, primarily for the purpose of initializing those attributes..
alsoitContext objectIt is applied in situations where we need to execute extra operations after initializing the object members.

Let's understand more about object references and return values with the following table

Object Reference

Object ReferenceUsed with functionsUse Case
thisrun, with, and apply functionsrefer to the context object by a lambda receiver keyword
itlet and also functionsrefer to the object’s context as a lambda argument.

Return Value

Return ValueUsed with functionsUse Case
Lambda result let, run, and with functionsWhen we include an expression at the conclusion of the code block, it transforms into the return value for the scope function.
Context objectapply and also functionsFunctions yield the context object itself, eliminating the need for explicit return value specification. The context object is automatically returned in this scenario.

Without the use of scope functions

Output:

With scope functions

Output:

Without scope functions: mention the object name each time to access class members. With scope functions: directly refer to members without specifying the object name.

Local functions

Local functions are functions defined within another function's scope, accessible only within that containing function, and typically used to encapsulate context-specific logic.

Code:

Output:

Explanation: In this example, there's a greet function defined inside the main function. It accepts a name parameter and prints a greeting message. The greet function is called twice with different names.

A local function can access the local variables of its outer function, creating a closure.

Code:

Output:

Explanation: In this example, outerFunction defines counter and localFunction. localFunction increments and prints counter, forming a closure. outerFunction returns localFunction, enabling it to retain access to counter even after execution. When myLocalFunction is called, it manipulates counter, illustrating closures in Kotlin.

Member functions

Member functions are functions defined within a class. They are accessible through instances of the class and can access the class's properties and methods.

Code:

Output:

As you can see, member functions are called with dot notation (.)

Generic vs Tail Recursive Functions

Generic Functions

Generic functions are designed to work with different types (classes or data types) in a flexible and reusable way

They allow us to write functions or classes that can operate on various data types without specifying the type in advance.

Type parameters are introduced using angle brackets <T>, and we can specify these parameters when calling the function. For example,

Code:

In order to invoke a generic function, there's a need to indicate the type arguments right after then function name at the call site Let's see an example,

Code:

Kotlin often times can deduce the type arguments from the context as well in such as case it can be defined as:

Tail Recursive functions

Kotlin supports tail recursion, a functional programming technique. It's handy for algorithms that would usually use loops. Tail recursion allows recursive functions without worrying about stack overflow errors. When a function is marked with tailrec and meets certain criteria, the compiler optimizes it into an efficient loop-based solution.

In this code, we aim to find the fixed point of the cosine function, starting from 1.0. We repeatedly apply cosine until the difference between the current value and its cosine is smaller than a specified precision eps. Code:

Explanation: The findFixedPoint function calculates a fixed point from an initialValue recursively until reaching the precision threshold. For tailrec, it must end with a self-call, excluding extra code after the recursive call or within specific contexts.

Conclusion

  1. To define a function in kotlin the keyword fun is used right before the function name
  2. Member functions are called with dot notation (.)
  3. Infix functions always require both the receiver and the parameter to be explicitly specified
  4. Generic functions are designed to work with different types (classes or data types) in a flexible and reusable way
  5. Data classes, tuples, or other data structures can be used to bundle multiple values together into a single return value
  6. Data types of actual and formal arguments must align