Cómo crear y usar funciones en Kotlin

Video thumbnail

No explicaré demasiado porque el concepto es universal en cualquier lenguaje: una función es una pieza de código reutilizable que realiza una tarea específica.

Lo que varía aquí es la palabra clave para definirlas. Mientras que en JavaScript se usa function, en Kotlin (por ejemplo) se utiliza fun. Su estructura básica consiste en la palabra reservada, el nombre de la función, los parámetros entre paréntesis y el cuerpo encerrado entre llaves {}.

Anteriormente vimos como usar los condicionales en Kotlin: if y when.

⚙️ Parámetros y Argumentos con Nombre

Los parámetros se separan por comas, como en el 99% de los lenguajes. Una característica muy útil es el uso de argumentos con nombre.

Esto permite invocar una función especificando el nombre del parámetro. Es extremadamente útil cuando las funciones tienen muchos argumentos o cuando no es obvio el orden de los mismos. Además, al usar nombres, puedes invertir el orden de los parámetros al llamarla sin que afecte el resultado, ya que el compilador sabe exactamente a qué variable te refieres.

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

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

Retorno

El uso de return es fundamental cuando queremos que la función realice un cálculo y nos devuelva el resultado. Por ejemplo, en una calculadora, retornaríamos el valor de una suma. Si la función solo imprime un "Hola Mundo", el retorno es opcional, ya que la tarea principal es la impresión por pantalla.

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

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

El tipo de retorno “Unit”

Por defecto, si una función no devuelve nada, lo que retorna es un tipo Unit. Sin embargo, como ocurre en cualquier lenguaje, puedes manejar múltiples puntos de retorno dentro de una misma función.

Retornos Múltiples

Esto es muy útil, por ejemplo, en las validaciones de datos. Si vas a devolver un valor basado en una condición, puedes usar la sentencia return para finalizar la ejecución en cuanto se cumpla una regla.

Ejemplo: Validación de Registro

Imagina una función que recibe un username y un email. La lógica funcionaría así:

  • Validación de reglas: Primero validas el username. Si no cumple con las reglas (por ejemplo, es muy corto), retornas inmediatamente un mensaje diciendo: "Este username es inválido".
  • Validación de existencia: Si pasa las reglas, pero el nombre ya está registrado en la base de datos, retornas: "El nombre de usuario ya está tomado".
  • Validación de formato de Email: Una vez validado el usuario, pasas al correo. Si el usuario ingresa algo como "patata" (sin formato de correo), retornas: "El email no es válido".
  • Validación de duplicidad de Email: Si el formato es correcto, como patata@correo.com, pero ya existe en el sistema, retornas: "El email ya ha sido tomado".

De esta manera, puedes tener múltiples retornos en tu función basados en lo que esté sucediendo en cada etapa del proceso.

// 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
}

Esquema Tradicional vs. Expresión Única

Es importante destacar que, cuando manejas esta lógica de validaciones con múltiples condiciones (if/else), debes regresar al esquema tradicional de funciones (usando llaves {} y return explícitos).

No podrías usar el formato de "función de una sola línea" o expresión única, ya que ese formato está diseñado para realizar una operación directa y terminar, mientras que aquí necesitas evaluar diferentes escenarios antes de dar una respuesta final.

Valores por Defecto (Parámetros Opcionales)

Al igual que en lenguajes como PHP o Python, podemos asignar valores por defecto. Esto es ideal para parámetros que casi siempre son iguales.

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
}

Ejemplo: Si tienes una plataforma de pagos y la mayoría usa PayPal, puedes definirlo por defecto. Así, si el usuario no especifica el método, la función asume automáticamente PayPal. Esto convierte al parámetro en opcional.

⚡ Funciones de Expresión Única

Si una función solo realiza una operación y retorna un valor, podemos simplificarla eliminando las llaves y el return, utilizando directamente el signo igual =. Esto ahorra líneas de código y hace que la función sea mucho más legible.

// Función tradicional
fun doble(x: Int): Int { return x * 2 }

// Función de expresión única
fun doble(x: Int) = x * 2

Funciones Lambda

Las funciones Lambda son aquellas que podemos asociar directamente a variables. Son muy útiles para modularizar tareas específicas que solo se ejecutarán dentro de un contexto particular.

Se suelen conocer también como "funciones de flecha" por su sintaxis:

  1. Definimos el parámetro.
  2. Colocamos la flecha ->.
  3. Escribimos la operación a realizar.
fun uppercaseString(text: String): String {
    return text.uppercase()
}
fun main() {
    println(uppercaseString("hello"))
    // HELLO
}

Las funciones de extensión en Kotlin

Las funciones de extensión son una poderosa herramienta que nos ofrece Kotlin al momento de desarrollar aplicaciones en Android.

Una manera de verlo es como si pudiéramos extender en base a funciones clases definidas ya sea por nosotros, de terceros o provistas por Kotlin de una manera rápida y sin complicaciones.

Por ejemplo, si quisiéramos extender el tipo de dato Int con una función que nos indica si es un número par:

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

Con esta función estamos expresando en una función que extiende de los tipos de datos enteros que verifique si un número es par o no; para emplearlo podemos hacer lo siguiente:

println(10.isEven())

La respuesta del compilador sería true.

Como indicamos en un inicio, también podemos emplear las funciones de extensión en otros tipos de estructuras como clase; por ejemplo, tomando la clase anterior Persona:

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
}

}

Podemos hacer lo siguiente:

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

También la podemos simplificar como:

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

Para este ejemplo, en la que la edad es menor a 60 años retorna false; si por el contrario, creamos una persona como:

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

La respuesta sería true.

Tipos nulos

Note que en base a los ejemplo que estamos viendo anteriormente, nada nos impide que empleemos funciones de extensión con variables nulas en un momento dado, esto podría ser un problema pero lo podemos solventar fácilmente realizando una comprobación previa e indicando el operador en cuestión luego de indicar el nombre de la clase en la función de expresión:

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

   return false;
}

println(NULL.isEven())

Protección de Nulos (Null Safety): Si queremos que nuestra función de extensión sea segura ante valores nulos, añadimos el signo de interrogación ? al tipo de dato (ej. Int?). Esto nos permite realizar verificaciones internas antes de operar, evitando que la aplicación se rompa (NullPointerException).

El código anterior, nos devolvería false; veamos otro ejemplo sobrescribiendo el método toString():

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

Y esto nos devuelve el texto nada.

Ahora, aprende a usar las clases en Kotlin.

Acepto recibir anuncios de interes sobre este Blog.

Guía completa sobre funciones en Kotlin: aprende sintaxis, parámetros nombrados, lambdas, funciones de extensión y manejo de nulos con ejemplos prácticos.

| 👤 Andrés Cruz

🇺🇸 In english