Functions in Kotlin
When Andrey Breslav, the initial Kotlin creator, was asked about his favourite feature during a discussion panel at KotlinConf Amsterdam, he said it was functions1. In the end, functions are our programs' most important building blocks. If you look at real-life applications, most of the code either defines or calls functions.
In Kotlin, we define functions using the
fun keyword. This is why we have so much "fun" in Kotlin. With a bit of creativity, a function can consist only of
This is the so-called identity function, a function that returns its argument without any modifications. It has a generic type parameter
Fun, but this will be explained in the chapter Generics.
By convention, we name functions using lower camelCase syntax0. Formally, we can use characters, underscore
_, and numbers (but not at the first position), but in general just characters should be used.
This is what a typical function looks like:
Notice that the parameter type is specified after the variable name and a colon, and the result type is specified after a colon inside the parameter brackets. Such notation is typical of languages with powerful support for type inference because it is easier to add or remove explicit type definitions.
To use a reserved keyword as a function name (like
when), use backticks, as in the example above. When a function has an illegal name, both its definition and calls require backticks.
Another use case for backticks is naming unit-test functions so that they can be described in plain English, as in the example below. This is not standard practice, but it is still quite a popular practice that many teams choose to adopt.
Many functions in real-life projects just have a single expression2, so they start and immediately use the
return keyword. The
square function defined above is a great example. For such functions, instead of defining the body with braces, we can use the equality sign (
=) and just specify the expression that calculates the result without specifying
return. This is single-expression syntax, and functions that use it are called single-expression functions.
An expression can be more complicated and take multiple lines. This is fine as long as its body is a single statement.
When we use single-expression function syntax, we can infer the result type. We don’t need to, as explicit result type might still be useful for safety and readability3, but we can.
Functions on all levels
Kotlin allows us to define functions on many levels, but this isn’t very obvious as Java only allows functions inside classes. In Kotlin, we can define:
- functions in files outside any classes, called top-level functions,
- functions inside classes or objects, called member functions (they are also called methods),
- functions inside functions, called local functions or nested functions.
Top-level functions (defined outside classes) are often used to define utils, small but useful functions that help us with development. Top-level functions can be moved and split across files. In many cases, top-level functions in Kotlin are better than static functions in Java. Using them seems intuitive and convenient for developers.
Take a look at the below example, which presents a function that validates a form. It checks conditions for the form fields. If a condition is not matched, we should show an error and change the local variable
false, in which case we should not return from the function because we want to check all the fields (we should not stop at the first one that fails). This is an example of where a local function can help us extract repetitive behavior.
Parameters and arguments
A variable defined as a part of a function definition is called a parameter. The value that is passed when we call a function is called an argument.
In Kotlin, parameters are read-only, so we cannot reassign their value.
If you need to modify a parameter variable, the only way is to shadow it with a local variable that is mutable.
This is possible but discouraged. A parameter holds a value that was used as an argument, and this value should not change. A local read-write variable represents a different concept and should therefore have a different name.
Unit return type
In Kotlin, all functions have a result type, so every function call is an expression. When a type is not specified, the default result type is
Unit, and the default result value is the
Unit is just a very simple object that is used as a placeholder when nothing else is returned. When you specify a function without an explicit result type, its result type will implicitly be
Unit. When you define a function without
return in the last line, it is the same as using
return with no value. Using
return with no value is the same as returning
Each parameter expects one argument, except for parameters marked with the
vararg modifier. Such parameters accept any number of arguments.
A good example of such a function is
listOf, which produces a list from values used as arguments.
This means a vararg parameter holds a collection of values, therefore it cannot have the type of a single object. So the vararg parameter represents an array of the declared type, and we can iterate over arrays using a for loop (which will be explained in more depth in the next chapter).
We will get back to vararg parameters in the chapter Collections, in the section dedicated to arrays.
Named parameter syntax and default arguments
When we declare functions, we often specify optional parameters. A good example is
joinToString, which transforms an iterable into a
String. It can be used without any arguments, or we might change its behavior with concrete arguments.
Many more functions in Kotlin use optional parametrization, but how is this done? It is enough to place an equality sign after a parameter and then specify the default value.
Values specified this way are created on-demand when there is no parameter for their position. This is not Python, therefore they are not stored statically, which is why it’s safe to use mutable values as default arguments.
In Python, the analogous code would produce
[1, 1], and
[1, 1, 1].
When we call a function, we can specify an argument’s position by its parameter name, like in the example below. This way, we can specify later optional positions without specifying previous ones. This is called named parameter syntax.
Named parameter syntax is very useful for improving our code’s readability. When an argument's meaning is not clear, it is better to specify a parameter name for it.
Naming arguments also prevents mistakes that are a result of changing parameter positions.
In the above example, without named arguments a developer might flip the
surname positions; if named arguments were not used here, this would lead to an incorrect name and surname in the object. Named arguments protect us from such situations.
It is considered a good practice to use the named arguments convention when we call functions with many arguments, some of whose meanings might not be obvious to developers reading our code in the future.
In Kotlin, we can define functions with the same name in the same scope (file or class) as long as they have different parameter types or a different number of parameters. This is known as function overloading. Kotlin decides which function to execute based on the types of the specified arguments.
A practical example of function overloading is providing multiple function variants for user convenience.
Methods with a single parameter can use the
infix modifier, which allows a special kind of function call: without the dot and the argument parentheses.
This notation is used by some functions from Kotlin stdlib (Standard Library), like the
xor bitwise operations on numbers (presented in the chapter Basic types, their literals and operations).
Infix notation is only for our convenience. It is an example of Kotlin syntactic sugar - syntax that is designed only to make things easier to read or express.
Regarding the position of operators or functions in relation to their operands or arguments, we use three kinds of position types: prefix, infix, and postfix. Prefix notation is when we place the operator or function before the operands or arguments8. A good example is a plus or minus placed before a single number (like
-3.14). One might argue that a top-level function call also uses prefix notation because the function name comes before the arguments (like
maxOf(10, 20)). Infix notation is when we place the operator or function between the operands or arguments6. A good example is a plus or minus between two numbers (like
1 + 2or
10 - 7). One might argue that a method call with arguments also uses infix notation because the function name comes between the receiver (the object we call this method on) and arguments (like
account.add(money)). In Kotlin, we use the term "infix notation" more restrictively to reference the special notation we use for methods with the
infixmodifier. Postfix notation is when we place the operator or function after the operands or arguments7. In modern programming, postfix notation is practically not used anymore. One might argue that calling a method with no arguments is postfix notation, as in
When a function declaration (name, parameters, and result type) is too long to fit in a single line, we split it such that every parameter definition is on a different line, and the beginning and end of the function declaration are also on separate lines.
Classes are formatted in the same way5:
When a function call4 is too long, we format it similarly: each argument is on a different line. However, there are exceptions to this rule, such as keeping multiple vararg parameters on the same line.
In this book, the width of my lines is much smaller than in regular projects, so I am forced to break lines much more often than I would like to.
Notice that when I specify arguments or parameters, I sometimes add a comma at the end. This is a so-called trailing comma. Such notation is optional.
I like using trailing comma notation because it makes it easier to add another element in the future. Without it, adding or removing an element requires not only a new line but also an additional comma after the last element. This leads to meaningless line modifications on Git, which makes it harder to read what has actually changed in our project. Some developers don’t like trailing comma notation, which can sometimes lead to a holy war. Decide in your team if you like it or not, and be consistent in your projects.
As you can see, functions in Kotlin have a lot of powerful features. Single-expression syntax makes simple functions shorter. Named and default arguments help us improve safety and readability. The
Unit result type makes every function call an expression. Vararg parameters allow any number of arguments to be used for one parameter position. Infix notation introduces a more convenient way to call certain kinds of functions. Trailing commas minimize the number of changes on git. All this is for our convenience. For now though, let's move on to another topic: using a for-loop.
This rule has some exceptions. For example, on Android, Jetpack Compose functions should be named using UpperCamelCase by convention. Also, unit tests are often named with full sentences inside braces.
As a reminder, an expression is a part of our code that returns a value.
See Effective Kotlin Item 4: Do not expose inferred types
A constructor call is also considered a function call in Kotlin.
We will discuss classes later in this book, in the chapter Classes and interfaces.
From the Latin word infixus, the past participle of infigere, which we might translate as "fixed in between".
Made from the prefix "post-", which means "after, behind", and the word "fix", meaning "fixed in place".
From the Latin word praefixus, which means "fixed in front".