How to create and use functions in Kotlin

Video thumbnail

I won't explain too much because the concept is universal across all languages: a function is a reusable piece of code that performs a specific task.

What varies here is the keyword used to define them. While JavaScript uses function, Kotlin (for example) uses fun. Its basic structure consists of the reserved word, the function name, parameters within parentheses, and the body enclosed in curly braces {}.

Previously we saw how to use conditionals in Kotlin: if and when.

⚙️ Parameters and Named Arguments

Parameters are separated by commas, as in 99% of languages. A very useful feature is the use of named arguments.

This allows invoking a function by specifying the parameter name. It is extremely useful when functions have many arguments or when their order is not obvious. Furthermore, by using names, you can reverse the order of the parameters when calling it without affecting the result, as the compiler knows exactly which variable you are referring to.

fun sum(x: Int, y: Int): Int {
    return x + y
}

fun main() {
    println(sum(1, 2))
    // 3
}

Return

The use of return is fundamental when we want the function to perform a calculation and give us back the result. For example, in a calculator, we would return the value of an addition. If the function only prints "Hello World", the return is optional, since the main task is printing to the screen.

fun hello() {
    return println("Hello, world!")
}

fun main() {
    hello()
    // Hello, world!
}

The “Unit” return type

By default, if a function returns nothing, it returns a Unit type. However, as in any language, you can handle multiple return points within the same function.

Multiple Returns

This is very useful, for example, in data validations. If you are going to return a value based on a condition, you can use the return statement to end execution as soon as a rule is met.

Example: Registration Validation

Imagine a function that receives a username and an email. The logic would work like this:

  • Rule validation: First, you validate the username. If it doesn't meet the rules (for example, it's too short), you return immediately with a message saying: "This username is invalid."
  • Existence validation: If it passes the rules but the name is already registered in the database, you return: "Username already taken."
  • Email format validation: Once the user is validated, you move to the email. If the user enters something like "potato" (without email format), you return: "The email is not valid."
  • Email duplication validation: If the format is correct, like potato@mail.com, but it already exists in the system, you return: "The email has already been taken."

In this way, you can have multiple returns in your function based on what is happening at each stage of the process.

// A list of registered usernames
val registeredUsernames = mutableListOf("john_doe", "jane_smith")

// A list of registered emails
val registeredEmails = mutableListOf("john@example.com", "jane@example.com")

fun registerUser(username: String, email: String): String {
    // Early return if the username is already taken
    if (username in registeredUsernames) {
        return "Username already taken. Please choose a different username."
    }

    // Early return if the email is already registered
    if (email in registeredEmails) {
        return "Email already registered. Please use a different email."
    }

    // Proceed with the registration if the username and email are not taken
    registeredUsernames.add(username)
    registeredEmails.add(email)

    return "User registered successfully: $username"
}

fun main() {
    println(registerUser("john_doe", "newjohn@example.com"))
    // Username already taken. Please choose a different username.
    println(registerUser("new_user", "newuser@example.com"))
    // User registered successfully: new_user
}

Traditional Scheme vs. Single Expression

It is important to note that when handling this validation logic with multiple conditions (if/else), you must return to the traditional function scheme (using curly braces {} and explicit returns).

You could not use the "one-line function" or single expression format, as that format is designed to perform a direct operation and finish, whereas here you need to evaluate different scenarios before providing a final answer.

Default Values (Optional Parameters)

Just like in languages like PHP or Python, we can assign default values. This is ideal for parameters that are almost always the same.

fun printMessageWithPrefix(message: String, prefix: String = "Info") {
    println("[$prefix] $message")
}

fun main() {
    // Function called with both parameters
    printMessageWithPrefix("Hello", "Log") 
    // [Log] Hello
    
    // Function called only with message parameter
    printMessageWithPrefix("Hello")        
    // [Info] Hello
    
    printMessageWithPrefix(prefix = "Log", message = "Hello")
    // [Log] Hello
}

Example: If you have a payment platform and most people use PayPal, you can set it as default. This way, if the user doesn't specify the method, the function automatically assumes PayPal. This makes the parameter optional.

⚡ Single Expression Functions

If a function only performs one operation and returns a value, we can simplify it by removing the braces and the return keyword, using the equal sign = directly. This saves lines of code and makes the function much more readable.

// Traditional function 
fun double(x: Int): Int { return x * 2 } 
// Single expression function 
fun double(x: Int) = x * 2

Lambda Functions

Lambda functions are those that we can associate directly with variables. They are very useful for modularizing specific tasks that will only run within a particular context.

They are also commonly known as "arrow functions" because of their syntax:

  1. We define the parameter.
  2. We place the arrow ->.
  3. We write the operation to be performed.
fun uppercaseString(text: String): String {
    return text.uppercase()
}
fun main() {
    println(uppercaseString("hello"))
    // HELLO
}

Extension Functions in Kotlin

Extension functions are a powerful tool that Kotlin offers us when developing Android applications.

One way to see it is as if we could extend classes—whether defined by us, third parties, or provided by Kotlin—based on functions in a fast and uncomplicated way.

For example, if we wanted to extend the Int data type with a function that tells us if it is an even number:

fun Int.isEven(): Boolean = (this % 2 == 0)

With this function, we are expressing in a function that extends from integer data types to verify if a number is even or not; to use it we can do the following:

println(10.isEven())

The compiler's response would be true.

As we indicated at the beginning, we can also use extension functions in other types of structures like classes; for example, taking the previous Persona class:

class Persona(nombre: String, apellido: String, edad: Int) {
var nombre: String = ""
var apellido: String = ""
var edad: Int = 0

init {
	this.nombre = nombre
	this.apellido = apellido
	this.edad = edad
}

}

We can do the following:

fun Persona.esTerceraEdad(): Boolean = edad >= 60

We can also simplify it as:

fun Persona.esTerceraEdad() = edad >= 60
var persona = Persona("Andrés"," Cruz",27)
println(persona.esTerceraEdad())

For this example, where the age is less than 60 years, it returns false; if, on the contrary, we create a person as:

var persona = Persona("Andrés"," Cruz",80)

The answer would be true.

Nullable Types

Note that based on the examples we are seeing previously, nothing prevents us from using extension functions with null variables at a given time; this could be a problem, but we can easily solve it by performing a prior check and indicating the operator in question after the class name in the expression function:

fun Int?.isEven(): Boolean {
   if (this != NULL) return this % 2 == 0

   return false;
}

println(NULL.isEven())

Null Safety: If we want our extension function to be safe against null values, we add the question mark ? to the data type (e.g., Int?). This allows us to perform internal checks before operating, preventing the application from breaking (NullPointerException).

The previous code would return false; let's see another example overriding the toString() method:

fun Any?.toString(): String {
   if (this == NULL) return "nada"
   return toString()
}
println(NULL.toString())

And this returns the text nada.

Now, learn how to use classes in Kotlin.

I agree to receive announcements of interest about this Blog.

Complete guide to functions in Kotlin: learn syntax, named parameters, lambdas, extension functions and null handling with practical examples.

| 👤 Andrés Cruz

🇪🇸 En español