CodeGym /Cursos /Kotlin SELF /forEach y efectos secundarios: cuando es apropiado

forEach y efectos secundarios: cuando es apropiado

Kotlin SELF
Nivel 22, Lección 0
Disponible

1. Que es forEach y en que se diferencia de for

Cuando ves forEach, la primera reaccion suele ser: “Ajá, es como el bucle for, solo que mas moderno”. Y si, realmente recorre los elementos. Pero su sentido es un poco distinto: forEach es una llamada a funcion para cada elemento, no una construccion del lenguaje. Por eso cambian los matices: que se puede/no se puede hacer dentro, como se lee el codigo y por que forEach casi siempre se pone donde hay que realizar una accion, y no “calcular algo y devolverlo”.

Si lo comparamos muy por encima, for es “yo controlo el proceso de iteracion”, y forEach es “la coleccion se itera a si misma y yo le doy la instruccion de que hacer con cada elemento”.

Conviene tener en mente esta tablita (no trata de “correcto/incorrecto”, sino de la intencion):

Herramienta Que es en Kotlin Objetivo tipico Valor devuelto
for (...) { ... }
construccion del lenguaje iteracion + control (break/continue) ninguno (es flujo de control)
forEach { ... }
funcion de orden superior “haz una accion para cada elemento”
Unit

En las Kotlin coding conventions se menciona aparte que, al elegir entre un bucle normal y forEach, a menudo conviene preferir for, salvo casos como un receiver nullable o cuando forEach forma parte de una cadena de llamadas.

2. La signatura de forEach y el papel de Unit

Cuando empiezas a usar funciones de colecciones, el cerebro tiene un habito astuto: “si es una funcion, entonces devuelve algo y puedo guardarlo en una variable”. En el caso de forEach, casi siempre no. forEach esta pensado para ejecutar codigo sobre cada elemento, y su “resultado” es un efecto secundario (por ejemplo, imprimir en consola). Por eso forEach devuelve Unit, es decir, “nada util como valor”.

Veamos un mini-ejemplo:

fun main() {
    val names = listOf("Ann", "Bob", "Chris")

    names.forEach { name ->
        println("Hello, $name") // Hello, Ann ... y asi sucesivamente
    }
}

Aqui todo es logico: queremos imprimir un saludo para cada nombre. Esa es precisamente la finalidad de forEach.

Y esto es lo que a menudo intentan hacer los principiantes:

fun main() {
    val xs = listOf(1, 2, 3)

    val result = xs.forEach { x ->
        println(x)
    }

    println(result) // aqui sera kotlin.Unit
}

result aqui es Unit. Es decir, “guardamos en una variable el hecho de que no devolvimos nada”. Si alguna vez coleccionaste envoltorios de caramelos, felicidades: ya estas listo para este estilo.

3. forEach para una accion: impresion, log, llamada a funcion

forEach es especialmente bueno cuando ya tienes datos preparados y quieres “aplicarlos” al mundo: mostrar en pantalla, escribir en el log, enviar a algun sitio, llamar a una funcion que realiza una accion clara. La senal mas honesta de que forEach es apropiado: dentro de la lambda haces algo “hacia fuera”, y no construyes una nueva estructura de datos.

Imprimir: el efecto secundario mas honesto

fun main() {
    val xs = listOf(10, 20, 30)
    xs.forEach { x ->
        println("x = $x") // x = 10, luego 20, luego 30
    }
}

Llamar a una funcion “con efecto”

Supongamos que tenemos una funcion que imprime una advertencia (es una accion, no un calculo):

fun warn(message: String) {
    println("WARNING: $message")
}

fun main() {
    val problems = listOf("Empty input", "Wrong format", "Out of range")
    problems.forEach { p ->
        warn(p) // WARNING: Empty input ...
    }
}

Un pequeno esquema: donde vive el sentido de forEach

A veces es util imaginarlo asi:

flowchart LR
    A[Data collection] --> B["forEach { ... }"]
    B --> C[ACTION: println / log / function call]

Esto no es “matematica”; es un “ejecutor”: datos → procesar → realizar una accion.

4. Trucos utiles con forEach

Callable reference ::println: cuando “mas corto” de verdad es mejor

Despues de las lambdas aprendiste las callable references: :: permite pasar una funcion ya hecha donde se espera una lambda. Queda especialmente bonito cuando la lambda seria “simplemente llama a una funcion con un argumento”.

Por ejemplo, en vez de:

fun main() {
    val xs = listOf(1, 2, 3)
    xs.forEach { x -> println(x) } // 1 2 3
}

puedes:

fun main() {
    val xs = listOf(1, 2, 3)
    xs.forEach(::println) // 1 2 3
}

Se ve como si lo leyeras “en lenguaje humano”: “para cada elemento — println”.

Aqui hay un punto importante de estilo: si la lambda se complica, es mejor nombrar el parametro explicitamente, y no usar it. En las Kotlin conventions tambien se resalta: lambdas cortas y sin configuracion — it; anidadas/complicadas — mejor con parametros explicitos.

?.forEach: un patron comodo para colecciones nullable

Hay un lugar donde forEach realmente le gana al for normal. Es el caso en que la coleccion puede ser null. Con for tendrias que escribir una comprobacion:

fun main() {
    val maybeItems: List<String>? = listOf("a", "b")

    if (maybeItems != null) {
        for (x in maybeItems) {
            println(x)
        }
    }
}

Y con forEach sale un patron directo y bonito:

fun main() {
    val maybeItems: List<String>? = listOf("a", "b")
    maybeItems?.forEach { println(it) } // a, luego b
}

Aqui se lee literalmente como una frase: “si la lista existe, realiza una accion para cada elemento”.

Y este es precisamente el caso que a menudo se considera justificado para forEach: cuando ?.forEach elimina un if extra y hace el codigo mas corto, sin convertirlo en un acertijo.

5. Efectos secundarios: que son y por que hay que llevarse con ellos con cuidado

La expresion “efecto secundario” suena como si ahora fueran a hablar de medicamentos. Pero en programacion es mas simple: un efecto secundario es cambiar algo fuera del valor devuelto. Imprimir en consola es un efecto secundario. Escribir en un archivo es un efecto secundario. Cambiar una variable externa tambien es un efecto secundario.

forEach se usa con mas frecuencia precisamente donde los efectos secundarios son apropiados. Solo es importante entenderlo: los efectos secundarios hacen que el codigo sea menos predecible si se abusa de ellos. Dicho de forma simple: si una funcion devuelve un numero, puedes comprobar su resultado. Si una funcion “cambio algo por ahi”, comprobarlo es mas dificil.

“Acumulamos la suma”: se puede, pero conscientemente

Este ejemplo es de la serie “se puede, pero recuerda lo que estas haciendo”:

fun main() {
    val xs = listOf(10, 20, 30)

    var sum = 0
    xs.forEach { x ->
        sum += x // cambiamos una variable externa (efecto secundario)
    }

    println("sum = $sum") // sum = 60
}

¿Por que esto se considera un efecto secundario? Porque sum esta fuera de forEach, y nosotros lo estamos modificando.

Fijate: no digo “esto esta prohibido”. Digo: “esto se puede, pero tienes que entender que entraste en modo de estado mutable”.

“Construimos una cadena”: tambien es un efecto secundario, pero a menudo practico

Ya que conoces StringBuilder, aqui tienes un ejemplo cuidado: no imprimimos en cada paso, sino que construimos un texto que mostraremos una sola vez:

fun main() {
    val names = listOf("Ann", "Bob", "Chris")
    val sb = StringBuilder()

    names.forEach { name ->
        sb.appendLine("Hello, $name")
    }

    print(sb.toString())
    // Hello, Ann
    // Hello, Bob
    // Hello, Chris
}

Tecnicamente esto tambien es un efecto secundario (modificamos sb), pero es “local” y comprensible: el builder se crea al lado, se usa aqui mismo, no se fuga hacia fuera, y eso hace el codigo mas legible.

6. forEach en un proyecto CLI: imprimir la lista de gastos

A partir de hoy empezamos a escribir Kotlin mas “orientado a colecciones”, y nuestro proyecto practico de CLI (un rastreador de gastos hipotetico) es un lugar excelente para aplicar forEach. En el proyecto ya tenemos una coleccion de gastos, y la primera tarea practica es mostrarlos de forma bonita. Es una accion: no queremos “obtener una nueva lista”; queremos mostrar al usuario los datos actuales.

Hasta que pasemos a clases, guardaremos un gasto como Triple(category, amount, note): no es ideal, pero refleja honestamente la etapa actual del curso: trabajamos con estructuras basicas. Mas adelante sera un tipo separado y la vida sera mas facil.

Mini-datos del proyecto

fun main() {
    val expenses = mutableListOf(
        Triple("food", 250, "coffee"),
        Triple("transport", 120, "bus"),
        Triple("food", 560, "groceries"),
    )

    println("Expenses:")
    expenses.forEach { expense ->
        println(expense)
    }
    // Expenses:
    // (food, 250, coffee)
    // (transport, 120, bus)
    // (food, 560, groceries)
}

Esto ya funciona, pero la salida queda “cruda”: Triple se imprime como (a, b, c).

Un poco mejor: formateamos cada linea

Hagamos una pequena funcion de formateo y la aplicamos mediante forEach:

fun formatExpense(e: Triple<String, Int, String>): String {
    val (category, amount, note) = e
    return "$category: $amount ₣  ($note)"
}

fun main() {
    val expenses = listOf(
        Triple("food", 250, "coffee"),
        Triple("transport", 120, "bus"),
    )

    expenses.forEach { e ->
        println(formatExpense(e))
    }
    // food: 250 ₣  (coffee)
    // transport: 120 ₣  (bus)
}

Fijate: forEach aqui esta totalmente en su elemento. Creamos una funcion “pura” formatExpense, y usamos forEach para la accion: imprimir el resultado.

Impresion en forma de tabla: alineacion sencilla

Ya que conoces padEnd() (y, en general, el trabajo basico con cadenas), se puede peinar un poco la salida:

fun main() {
    val expenses = listOf(
        Triple("food", 250, "coffee"),
        Triple("transport", 120, "bus"),
    )

    expenses.forEach { (category, amount, note) ->
        val left = category.padEnd(10)
        println("$left | ${amount.toString().padStart(5)} | $note")
        // food       |   250 | coffee
        // transport  |   120 | bus
    }
}

Aqui mostramos otro truco agradable: desestructuracion directamente en los parametros de la lambda. Esto hace el codigo mas corto y legible cuando el elemento tiene varios componentes.

“No toques la coleccion que estas recorriendo”

Como forEach es una iteracion, a muchos les entra la tentacion de hacer dentro algo como expenses.remove(...). Desde el punto de vista del sentido comun, es como “serrar la rama en la que estas sentado”: a veces obtendras un error, a veces un comportamiento extrano.

En este curso podemos acordar lo siguiente: dentro de forEach no modificamos la coleccion que estamos recorriendo. Para modificaciones habra estrategias mas adecuadas (y discusiones aparte), y hoy mantenemos forEach como herramienta de “recorrer y realizar una accion”.

7. Errores tipicos al trabajar con forEach

Error n.º 1: esperar que forEach construya una nueva lista.
Una trampa muy comun: “ahora con forEach multiplicare los numeros por 10 y obtendre numeros nuevos”. Pero forEach devuelve Unit, o sea que no va de resultado, sino de accion. Si aun asi recoges manualmente una lista nueva mediante mutableListOf() y add(), funciona, pero en la practica estas escribiendo una transformacion mediante efectos secundarios. No es una catastrofe, pero ese codigo se vuelve menos legible rapidamente, y ya en la siguiente leccion apareceran herramientas que expresan la intencion “obten una lista nueva” de forma directa.

Error n.º 2: esconder logica de negocio compleja dentro de forEach.
Cuando dentro de la lambda tienes 15 lineas, tres if y el intento de recordar como llegaste aqui, es que ya toca extraer el codigo a una funcion aparte. forEach es bueno cuando la lambda es corta o cuando llama a una funcion comprensible. Si no, leerlo se convierte en una mision: “encuentra donde termina la lambda y por que todo sigue dentro de ella”.

Error n.º 3: acumular estado mediante variables externas y sorprenderse con los bugs.
var sum = 0 y sum += x dentro de forEach es legal, pero si hay varias variables externas de este tipo, empiezas a escribir un “mini-programa con estado oculto”. Luego se anade un return temprano, una excepcion u otra pasada, y el resultado se vuelve dificil de explicar. Los efectos secundarios hay que usarlos como un cuchillo afilado: para lo que toca y sin agitarlo.

Error n.º 4: intentar usar break/continue como en un bucle normal.
forEach no es un bucle del lenguaje, por eso break dentro no funciona. En Kotlin hay opciones como el labeled return (return@forEach), pero es otro tema, y a los principiantes normalmente les anade mas confusion que beneficios. Si necesitas un verdadero “control del bucle” (parar, saltarte una iteracion, salir por condicion), casi siempre es mas sencillo y honesto usar un for normal.

Error n.º 5: abusar de it y perder el sentido del parametro.
it es bueno cuando la lambda es corta y obvia. Pero si dentro accedes a varios campos, formateas una cadena y haces condiciones, es mejor nombrar el parametro (expense, name, x). Asi el codigo se lee como texto y no como un jeroglifico. La Kotlin style guide apoya directamente este enfoque: lambdas cortas — it; anidadas/complicadas — parametros explicitos.

1
Tarea
Kotlin SELF, nivel 22, lección 0
Bloqueada
Saludos a los jugadores
Saludos a los jugadores
1
Tarea
Kotlin SELF, nivel 22, lección 0
Bloqueada
Lista de palabras
Lista de palabras
1
Tarea
Kotlin SELF, nivel 22, lección 0
Bloqueada
Registros de advertencias
Registros de advertencias
1
Tarea
Kotlin SELF, nivel 22, lección 0
Bloqueada
Suma de pagos
Suma de pagos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION