Introducción
Si has llegado hasta aquí, ya sabes cómo crear variables, manejar distintos tipos de datos, organizar información en colecciones y controlar el flujo de tu programa con condicionales y bucles. ¡Enhorabuena! Tienes las bases. Pero ahora toca dar el siguiente paso lógico en la programación: las funciones.
Imagina que en tu código tienes que hacer un cálculo complejo o mostrar un mensaje específico docenas de veces a lo largo del programa. ¿Vas a copiar y pegar el mismo bloque de código una y otra vez? Para eso están las funciones. En esta unidad, vamos a ver cómo Kotlin hace que declarar y usar funciones sea un proceso muy limpio, adentrándonos por último en el fascinante mundo de las expresiones lambda, una de las características más apreciadas por los desarrolladores modernos.
¿Qué son las funciones y cómo se declaran?
En Kotlin, puedes declarar tus propias funciones utilizando la palabra reservada fun (muy apropiado, ¿verdad? ¡Programar en Kotlin es divertido!). Una función no es más que un bloque de código al que le damos un nombre y que realiza una tarea concreta.
fun sumar(x: Int, y: Int): Int {
return x + y
}
fun main() {
println(sumar(1, 2))
// Salida: 3
}
Analizando el código paso a paso:
fun: La palabra clave mágica para decirle a Kotlin que vamos a crear una función.- Los parámetros: Se escriben siempre entre paréntesis
(). Cada parámetro debe tener un tipo de dato explícito (en este casoxeyson de tipoInt), y si hay varios, se separan por comas. Son los datos que la función necesita para poder trabajar. - El tipo de retorno: Después de los paréntesis de los parámetros y separado por dos puntos
:, indicamos qué tipo de dato va a devolver la función como resultado final. En este caso, devolverá un número entero (Int). - El cuerpo de la función: Es todo lo que va dentro de las llaves
{}. return: Usamos esta palabra para escupir o devolver el resultado de nuestro cálculo y dar por terminada la ejecución de la función.
El dato: Las convenciones de código oficiales de Kotlin recomiendan nombrar las funciones empezando con minúscula y usando camelCase (por ejemplo,
calcularPrecioTotal,imprimirUsuario), sin usar guiones bajos.
Parámetros nombrados
A veces, cuando llamas a una función que tiene muchísimos parámetros, el código puede volverse un poco lioso a simple vista. ¿Qué era ese true o ese 5 que le pasábamos a la función en la tercera posición? Kotlin nos permite usar parámetros nombrados.
Si incluyes el nombre del parámetro explícitamente al llamar a la función, no solo haces que tu código sea muchísimo más fácil de leer, sino que además puedes poner los parámetros en el orden que quieras.
fun imprimirMensaje(mensaje: String, prefijo: String) {
println("[$prefijo] $mensaje")
}
fun main() {
// Usamos argumentos nombrados y además invertimos el orden
imprimirMensaje(prefijo = "LOG", mensaje = "Sistema iniciado correctamente")
// Resultado: [LOG] Sistema iniciado correctamente
}
Valores por defecto
En lenguajes más antiguos, si a veces querías llamar a una función pasándole tres parámetros y otras veces solo dos, tenías que crear varias versiones de la misma función (esto se conoce como sobrecarga de métodos).
En Kotlin, te ahorras todo ese trabajo. Puedes definir valores por defecto para los parámetros utilizando el operador de asignación =. Si al llamar a la función omites ese parámetro, Kotlin utilizará automáticamente el valor por defecto que configuraste.
fun imprimirMensaje(mensaje: String, prefijo: String = "INFO") {
println("[$prefijo] $mensaje")
}
fun main() {
// Llamamos a la función pasándole ambos parámetros
imprimirMensaje("Usuario conectado", "LOG")
// Resultado: [LOG] Usuario conectado
// Llamamos a la función pasándole SOLO el mensaje. ¡Usa el prefijo por defecto!
imprimirMensaje("El proceso ha finalizado")
// Resultado: [INFO] El proceso ha finalizado
}
La regla de oro: Puedes saltarte parámetros concretos que tengan valores por defecto, pero si omites uno y quieres pasar el siguiente, tendrás que usar obligatoriamente parámetros nombrados para los que pongas a continuación.
Funciones que no devuelven nada
Si tu función simplemente realiza una acción (como imprimir un texto por la consola, reproducir un sonido o guardar un dato en la base de datos) pero no necesita devolverte ningún resultado matemático o lógico útil, su tipo de retorno en Kotlin es Unit (el equivalente al void en Java o C).
La maravilla de Kotlin es que no hace falta que escribas Unit ni que pongas un return. El compilador ya lo sabe.
fun saludar(nombre: String) {
println("¡Hola, $nombre!")
// Escribir `: Unit` arriba junto a los paréntesis o `return Unit` aquí abajo es totalmente opcional (y casi nadie lo hace).
}
Funciones de una sola expresión
Para hacer tu código aún más conciso y espectacular, si tu función consta de una única instrucción que devuelve un valor, puedes ahorrarte de un plumazo las llaves {} y la palabra return. Solo tienes que usar el signo igual =.
Tomemos la función sumar del principio:
fun sumar(x: Int, y: Int): Int {
return x + y
}
Se puede transformar en esta maravilla de una sola línea:
fun sumar(x: Int, y: Int) = x + y
Fíjate bien, ¡ni siquiera hemos puesto : Int! Como Kotlin cuenta con una característica llamada inferencia de tipos, deduce inmediatamente que si sumas dos Int, el resultado será forzosamente un Int.
(No obstante, si estás trabajando en equipo y quieres que otros programadores lean tu código rápidamente, nunca está de más indicar explícitamente el tipo de retorno: fun sumar(x: Int, y: Int): Int = x + y).
Retornos tempranos en funciones
En ocasiones, quieres salir de una función inmediatamente si se cumple (o no) una condición, sin necesidad de seguir ejecutando y procesando el resto del código. Para eso usamos la palabra return dentro de un condicional if.
val usuariosRegistrados = mutableListOf("juan_perez", "maria_lopez")
fun registrarUsuario(usuario: String): String {
// Retorno temprano si el usuario ya existe en nuestra base de datos (lista)
if (usuario in usuariosRegistrados) {
return "Error: El nombre de usuario ya está pillado. Elige otro."
}
// Si no hemos entrado en el if anterior, la función continúa de forma normal
usuariosRegistrados.add(usuario)
return "¡Usuario $usuario registrado con éxito!"
}
fun main() {
println(registrarUsuario("juan_perez"))
// Salida: Error: El nombre de usuario ya está pillado. Elige otro.
println(registrarUsuario("nuevo_user"))
// Salida: ¡Usuario nuevo_user registrado con éxito!
}
Expresiones lambda
Kotlin te permite escribir funciones de forma todavía más compacta usando lo que conocemos como expresiones lambda. Básicamente, una lambda es una función anónima (sin nombre) que puedes guardar directamente en una variable o pasarla como si fuera un parámetro más a otras funciones.
Mira esta función normal:
fun aMayusculas(texto: String): String {
return texto.uppercase()
}
Podemos escribir exactamente lo mismo como una expresión lambda y guardarla en una variable:
val aMayusculasLambda = { texto: String -> texto.uppercase() }
fun main() {
println(aMayusculasLambda("hola"))
// Resultado: HOLA
}
Estructura de una lambda:
- Se encapsula siempre entre llaves
{ }. - Primero van los parámetros (
texto: String). - Luego una flecha
->. - Y finalmente el cuerpo de la función (lo que hace y lo que se devuelve de forma automática:
texto.uppercase()).
Pasando lambdas a otras funciones
Esto resulta muy útil cuando trabajamos con colecciones (listas, sets, mapas…). Multitud de funciones nativas de colecciones reciben una lambda para saber exactamente qué deben hacer con cada uno de los elementos.
El mejor ejemplo es usar .filter() (para filtrar elementos en base a una condición) o .map() (para transformar los elementos de una lista en otros):
fun main() {
val numeros = listOf(1, -2, 3, -4, 5, -6)
// Quedarnos solo con los positivos. La lambda comprueba si el número es mayor que 0.
val positivos = numeros.filter({ x -> x > 0 })
println(positivos) // [1, 3, 5]
// Multiplicar todos los elementos de la lista por 2 usando map
val dobles = numeros.map({ x -> x * 2 })
println(dobles) // [2, -4, 6, -8, 10, -12]
}
Si la lambda es el último o el único parámetro que le pasas a una función (como pasa arriba con filter y map), Kotlin te permite sacarla fuera de los paréntesis
(). E incluso puedes omitir los paréntesis por completo si es el único argumento.
Reescribiendo lo anterior de forma idiomática:
val positivos = numeros.filter { x -> x > 0 }
Tipos de funciones
Al igual que una variable puede ser de tipo String o Int, ¡las funciones también tienen su propio tipo! La sintaxis para definir el tipo de una función consiste en poner los parámetros de entrada entre paréntesis y, tras una flecha ->, el tipo que va a devolver.
(String) -> String(recibe un texto, devuelve un texto)(Int, Int) -> Int(recibe dos enteros, devuelve un entero)() -> Unit(no recibe absolutamente nada, no devuelve nada útil)
Ejemplo declarando el tipo de forma explícita en una variable:
val sumarLambda: (Int, Int) -> Int = { a, b -> a + b }
Ejercicios
Aquí tienes varios ejercicios de menos a más dificultad (algunos adaptados de la documentación oficial y otros extra para asentar todo lo aprendido). Abre tu Kotlin Playground y vamos a practicar lo aprendido.
El área del círculo
Escribe una función normal llamada areaCirculo que reciba el radio de un círculo en formato entero (Int) y devuelva el área de ese círculo (con decimales).
Pista: Vas a necesitar importar la constante Pi desde kotlin.math.PI. La fórmula es PI * radio * radio.
import kotlin.math.PI
// Escribe tu función aquí
fun main() {
println(areaCirculo(2)) // Debería dar aprox 12.566...
}
El área del círculo en una sola línea
Reescribe la función areaCirculo del ejercicio anterior pero simplifícala para convertirla en una función de una sola expresión (Single-expression function), eliminando las llaves y el return.
Pasando el tiempo a segundos
Tienes una función que traduce un intervalo de tiempo (horas, minutos y segundos) a segundos totales. La inmensa mayoría de las veces, solo vas a querer pasarle un parámetro y que el resto sean cero por defecto.
Mejora la función y las llamadas en el main utilizando valores por defecto y argumentos nombrados para que el código quede impecable y súper legible.
// Mejora esta función
fun intervaloEnSegundos(horas: Int, minutos: Int, segundos: Int) =
((horas * 60) + minutos) * 60 + segundos
fun main() {
// Mejora estas llamadas
println(intervaloEnSegundos(1, 20, 15))
println(intervaloEnSegundos(0, 1, 25))
println(intervaloEnSegundos(2, 0, 0))
println(intervaloEnSegundos(0, 10, 0))
println(intervaloEnSegundos(1, 0, 1))
}
Generador de URLs
Tienes una lista de acciones de una API, un prefijo de una web y el ID de un recurso. Utilizando la función de colección .map() y una expresión lambda, crea una nueva lista de URLs que combinen los tres elementos para formar rutas completas (ejemplo esperado: https://ejemplo.com/libro/5/titulo).
fun main() {
val acciones = listOf("titulo", "anyo", "autor")
val prefijo = "https://ejemplo.com/libro"
val id = 5
val urls = // ¡Escribe aquí tu map con lambda!
println(urls)
}
Repetidor de acciones
Escribe una función llamada repetirN que reciba un número entero n y una acción (una función de tipo () -> Unit). La función debe repetir esa acción el número de veces indicado utilizando un bucle clásico. Luego, en el main, úsala (usando una trailing lambda) para imprimir "¡Me encanta Kotlin!" 5 veces.
El saludo personalizado
Crea una función llamada saludoPersonalizado que reciba un nombre (String), una edad (Int) y una ciudad (String). El parámetro ciudad debe tener el valor por defecto "Madrid".
La función debe usar un println() para mostrar: "Hola, me llamo [nombre], tengo [edad] años y vivo en [ciudad].".
Luego, en el main, llama a la función de dos formas distintas:
- Pasando solo nombre y edad.
- Pasando nombre, edad y ciudad, pero utilizando argumentos nombrados en un orden distinto al original.
Filtrando palabras largas
Dada la siguiente lista de palabras: val palabras = listOf("sol", "murciélago", "pan", "ornitorrinco", "luz", "desarrollador")
Utiliza la función .filter() y una trailing lambda para crear una nueva lista que solo contenga las palabras que tengan estrictamente más de 5 letras. (Pista: Los textos o Strings en Kotlin tienen una propiedad llamada .length que te dice lo largos que son). Imprime el resultado final.
Soluciones a los ejercicios
No hagas trampas. Échale un vistazo a las soluciones únicamente cuando te hayas peleado un buen rato con el código.
El área del círculo
import kotlin.math.PI
fun areaCirculo(radio: Int): Double {
return PI * radio * radio
}
fun main() {
println(areaCirculo(2))
// 12.566370614359172
}
El área del círculo en una sola línea
import kotlin.math.PI
// Quitamos llaves y return, y usamos el signo '='. Mantener el ': Double' es buena práctica.
fun areaCirculo(radio: Int): Double = PI * radio * radio
fun main() {
println(areaCirculo(2))
}
Pasando el tiempo a segundos
// Asignamos '= 0' a todos los parámetros para darles un valor por defecto
fun intervaloEnSegundos(horas: Int = 0, minutos: Int = 0, segundos: Int = 0) =
((horas * 60) + minutos) * 60 + segundos
fun main() {
println(intervaloEnSegundos(1, 20, 15)) // Este lo pasamos normal
println(intervaloEnSegundos(minutos = 1, segundos = 25)) // Omitimos las horas
println(intervaloEnSegundos(horas = 2)) // Omitimos minutos y segundos
println(intervaloEnSegundos(minutos = 10))
println(intervaloEnSegundos(horas = 1, segundos = 1))
}
Generador de URLs
fun main() {
val acciones = listOf("titulo", "anyo", "autor")
val prefijo = "https://ejemplo.com/libro"
val id = 5
// Usamos map para transformar cada elemento. Recuerda usar plantillas de texto ($) para fusionarlo todo.
val urls = acciones.map { accion -> "$prefijo/$id/$accion" }
println(urls)
// [https://ejemplo.com/libro/5/titulo, https://ejemplo.com/libro/5/anyo, https://ejemplo.com/libro/5/autor]
}
Repetidor de acciones
// El tipo de la acción es () -> Unit porque es un bloque de código que no recibe nada y no devuelve nada útil.
fun repetirN(n: Int, accion: () -> Unit) {
for (i in 1..n) {
accion()
}
}
fun main() {
// Como la acción es el último parámetro, podemos sacar las llaves (trailing lambda)
repetirN(5) {
println("¡Me encanta Kotlin!")
}
}
El saludo personalizado
fun saludoPersonalizado(nombre: String, edad: Int, ciudad: String = "Madrid") {
println("Hola, me llamo $nombre, tengo $edad años y vivo en $ciudad.")
}
fun main() {
// Llamada 1: usamos el valor por defecto para ciudad
saludoPersonalizado("Carlos", 28)
// Llamada 2: usamos argumentos nombrados alterando el orden natural
saludoPersonalizado(ciudad = "Valencia", edad = 32, nombre = "Laura")
}
Filtrando palabras largas
fun main() {
val palabras = listOf("sol", "murciélago", "pan", "ornitorrinco", "luz", "desarrollador")
// filter con trailing lambda. Solo conservamos la palabra si su longitud es mayor a 5.
val palabrasLargas = palabras.filter { palabra -> palabra.length > 5 }
println(palabrasLargas)
// [murciélago, ornitorrinco, desarrollador]
}