Criterio: cuándo aclaran y cuándo esconden
La mecánica de las funciones de ámbito se aprende en una tarde y su uso correcto se discute durante años, porque no es un problema técnico sino editorial. Esta lección cierra el nivel con el criterio: qué información concreta aporta cada función al lector cuando se elige bien y qué información destruye cuando se elige por costumbre, cómo se degrada la legibilidad al anidar bloques donde this e it se solapan sin que el compilador proteste, qué reglas de equipo han demostrado funcionar en bases de código grandes y cuáles solo generan discusiones sin efecto, y cómo revisar código ajeno buscando los síntomas del abuso en lugar de la preferencia personal.
Ninguna de las funciones de este nivel añade capacidad al lenguaje. Todo lo que se escribe con ellas se puede escribir sin ellas, con una variable local de más y algún punto repetido, y el código generado es equivalente porque todas se insertan en el punto de llamada. Esa observación, lejos de restarles importancia, la sitúa donde corresponde: si no aportan capacidad, entonces todo su valor está en lo que comunican, y una herramienta cuyo valor entero es comunicativo se juzga con criterios de redacción y no de ingeniería. La pregunta correcta ante cada uso no es si funciona, porque funciona siempre, ni si es más corto, porque casi siempre lo es; la pregunta es si el lector que llegue dentro de un año a esa línea sabrá más o sabrá menos gracias a ella. Este cierre del nivel trata de aprender a formular esa pregunta con precisión y de responderla en equipo sin convertirla en una guerra de preferencias.
- Enunciar qué información concreta comunica cada función de ámbito y verificar si el uso concreto la respeta.
- Identificar los patrones en que una función de ámbito destruye información en lugar de aportarla.
- Desmontar bloques anidados donde
thiseitse solapan y elegir la reescritura que recupera la claridad. - Formular convenciones de equipo verificables sobre estas funciones y descartar las que no lo son.
Lo que cada elección le promete al lector
Elegir bien una función de ámbito equivale a colocar una etiqueta al principio de un bloque, y esa etiqueta es una promesa. apply promete que dentro se configura el receptor y que fuera sale el mismo objeto. also promete que lo de dentro es un añadido y que la expresión sigue igual sin él. let promete que va a salir otra cosa. run y with prometen un cálculo sobre un objeto que ya se conoce. takeIf promete una condición cuyo incumplimiento se traduce en ausencia. Cuando la promesa se respeta, el lector puede saltarse el bloque en una primera pasada y confiar en la etiqueta; y esa capacidad de saltar es la mitad de lo que hace que un código grande se pueda leer.
// Promesas respetadas: cada nombre anticipa lo que hay dentro
val cliente = Cliente().apply { tiempoDeEspera = 30 }
val id = respuesta.let { extraerId(it) }
val guardado = usuario.also { auditoria.registrar(it) }
Conviene subrayar que esta promesa no es un convenio informal entre programadores educados: dos de ellas están respaldadas por el sistema de tipos, porque apply y also declaran su bloque con retorno Unit y por tanto el compilador impide que el valor calculado dentro escape. Las demás promesas son puramente semánticas y solo las sostiene quien escribe. Esa asimetría explica por qué los abusos más dañinos ocurren precisamente en las funciones que devuelven el resultado del bloque: ahí nada impide que un let haga tres cosas a la vez y devuelva la última.
Hay una consecuencia de esto que conviene enunciar sin rodeos porque va contra la intuición de quien viene de otros lenguajes: en Kotlin el nombre de una de estas funciones es parte de la documentación de la línea, y es la única parte que se lee siempre. Los comentarios se saltan, los nombres de variables se abrevian, los tipos se infieren y desaparecen del texto; el nombre de la función de ámbito está siempre ahí, en el sitio exacto donde empieza el bloque. Desperdiciar ese hueco eligiendo la función por costumbre es tirar el único espacio publicitario garantizado que tiene una expresión.
De ahí sale la única prueba que se necesita para revisar un uso concreto, y conviene aplicarla mecánicamente. Tapa el cuerpo del bloque, lee solo el nombre de la función y el receptor, y escribe qué esperas encontrar dentro. Después destapa. Si lo que hay coincide, la elección es correcta. Si no coincide, el nombre está mintiendo, y un nombre que miente es peor que no tener nombre porque induce a no leer lo que sí habría que leer.
Ante cualquier duda entre let y also, o entre run y apply, borra mentalmente el bloque completo. Si el resto del programa sigue compilando y haciendo lo esencial, el bloque era un añadido y le corresponde una función que devuelve el receptor. Si al borrarlo se pierde algo que el flujo necesitaba, era una etapa y le corresponde una función que devuelve el resultado. La prueba se aplica en segundos y resuelve la inmensa mayoría de las discusiones.
Cuándo esconden en lugar de aclarar
Hay cuatro formas típicas de destruir información con estas funciones, y todas comparten un origen: usarlas por hábito antes de saber qué se quiere decir. La primera es el envoltorio vacío, un let sobre un valor no nulable cuyo bloque simplemente llama a algo con it; no acota, no transforma y añade una indirección donde no había ninguna. La segunda es la etiqueta falsa, un also que muta o un apply cuyo bloque calcula, donde el nombre promete una cosa y el cuerpo hace otra. La tercera es la ramificación disfrazada, un ?.let con un elvis de varias líneas que en realidad es un if con dos ramas escritas a alturas visuales distintas. La cuarta es el bloque desbordado, un apply de veinte líneas que ya no es una configuración sino un procedimiento sin nombre.
// Ramificacion disfrazada: dos ramas que no se leen como dos ramas
val plan = usuario?.let {
val nivel = calcularNivel(it)
construirPlan(it, nivel)
} ?: run {
registrarAnonimo()
Plan.porDefecto()
}
// Lo mismo con las dos ramas a la misma altura
val plan = if (usuario == null) {
registrarAnonimo()
Plan.porDefecto()
} else {
construirPlan(usuario, calcularNivel(usuario))
}
Las dos versiones ocupan casi el mismo espacio y producen el mismo código, de modo que la elección se decide únicamente por lo que comunican. La primera presenta una rama como el caso principal y la otra como un apéndice pegado detrás de un operador, cuando en realidad ambas son igual de probables y de importantes. La segunda las coloca a la misma altura, que es exactamente lo que el lector necesita para evaluarlas en paralelo. Ese criterio, el de la altura visual de las alternativas, resuelve casi todos los casos en que alguien duda entre una expresión compacta y una bifurcación explícita.
flowchart TD
A[Voy a escribir una funcion de ambito] --> B{Que promete el nombre que he elegido}
B --> C{Coincide con lo que hay dentro del bloque}
C -- No --> D[Cambia la funcion o parte el bloque]
C -- Si --> E{El bloque cabe en la pantalla sin anidar}
E -- No --> F[Extrae una funcion con nombre]
E -- Si --> G[La eleccion comunica y se queda]El anidamiento que confunde this con it
El fallo más costoso del nivel no es elegir mal una función sino anidar dos y perder de vista a qué objeto se refiere cada nombre. El mecanismo es sencillo y por eso resulta traicionero: los receptores implícitos se apilan y el más interno gana; los parámetros implícitos llamados it también se apilan y el más interno oculta al externo. En ninguno de los dos casos el compilador emite advertencia alguna, porque la resolución es exactamente la que las reglas prescriben, de modo que el programa compila, ejecuta y hace algo distinto de lo previsto.
// Ilegible: un receptor implicito y un it compitiendo en el mismo bloque
pedido.apply {
lineas.forEach {
it.precio = it.precio * descuento // descuento de quien
historial.add(it.id) // historial de quien
}
}
En el bloque anterior hay cuatro nombres sin cualificar y ninguno de ellos declara a quién pertenece. Dos vienen del receptor del apply, uno del parámetro implícito del forEach y el cuarto podría venir de cualquiera de los dos o de la clase envolvente. Un lector honesto no puede resolverlo sin abrir las definiciones de ambos tipos, y un lector apresurado resolverá mal. Nada de eso produce un error de compilación, que es exactamente lo que convierte a este patrón en el más caro del nivel.
La reescritura correcta se apoya en dos decisiones. La primera es nombrar todos los parámetros en cuanto haya dos lambdas encajadas, lo que solo es posible en las funciones que entregan el objeto como argumento y es la razón principal para preferir also y let en contextos anidados. La segunda es no anidar dos funciones con receptor sobre tipos relacionados, porque ahí ni el lector ni el editor distinguen a quién pertenece cada miembro y la cualificación con arroba deja de ser una ayuda para convertirse en un parche.
pedido.also { p ->
p.lineas.forEach { linea ->
linea.precio = linea.precio * p.descuento
p.historial.add(linea.id)
}
}
La versión con nombres es dos identificadores más larga y cualquier lector puede verificarla sin abrir una sola definición. Merece la pena insistir en que el problema no era el apply ni el forEach, sino la coexistencia de dos objetos y cero nombres en el mismo bloque; la misma línea con un solo objeto habría sido impecable con receptor implícito. Por eso la regla útil no habla de funciones sino de cuántos objetos distintos toca un bloque, que es la variable que realmente decide si el código se puede leer.
Un implícito por bloque
Dentro de un bloque puede haber como máximo un nombre implícito activo. En cuanto aparece el segundo, nómbralos todos.
Nada de receptores anidados
Dos funciones con receptor encajadas sobre tipos parecidos producen errores que compilan. Convierte la exterior en also con nombre.
El bloque cabe en la pantalla
Si un bloque de ámbito no se ve entero de una vez, ha dejado de ser un modificador de una expresión y es un procedimiento sin nombre.
Convenciones de equipo que funcionan
Casi todos los equipos que discuten sobre estas funciones acaban escribiendo reglas que no se pueden verificar, del tipo no abuses o úsalas con criterio, y esas reglas no cambian ninguna revisión porque no distinguen ningún caso concreto. Las convenciones que sí funcionan comparten tres propiedades: son verificables leyendo una sola expresión, se pueden justificar por lo que comunican y no por gusto, y admiten excepciones argumentadas en lugar de prohibir. Cuatro han demostrado sobrevivir en bases de código grandes.
La primera es una función por bloque y ningún anidamiento de funciones de ámbito; cuando haga falta anidar, se extrae una función con nombre. La segunda es que ningún bloque supere las líneas que caben en la pantalla, con el mismo remedio. La tercera es que todo parámetro implícito se nombre en cuanto exista una segunda lambda en el mismo ámbito visual. La cuarta es que la elección se justifique por el eje del retorno, es decir, que apply y also no aparezcan nunca donde el valor del bloque importe ni let y run donde no importe. Las cuatro se comprueban de un vistazo y ninguna depende de la preferencia de quien revisa.
Conviene además decidir de antemano qué discusiones no se van a tener, porque consumen tiempo de revisión sin mejorar ninguna base de código. Prohibir una función concreta del conjunto nunca ha funcionado, porque el hueco que deja se rellena con la más parecida y el problema de fondo, que era la longitud del bloque o el anidamiento, permanece intacto. Obligar a usarlas siempre que sea posible es peor, porque convierte una decisión editorial en un requisito y produce exactamente los envoltorios vacíos que nadie quiere leer. Y discutir sobre run frente a with es tiempo perdido: ocupan el mismo casillero, la elección la dicta la posición del objeto en la expresión y ninguna revisión debería detenerse ahí.
Antes de añadir una regla sobre estas funciones a la guía de estilo del equipo, busca en el repositorio dos fragmentos reales que la regla habría cambiado. Si no aparecen, la regla no describe un problema que tengáis y solo servirá para dar autoridad a un gusto personal durante las revisiones. Si aparecen, pégalos en la propia guía: una convención con ejemplo se aplica sola y una convención abstracta se discute cada vez.
Conviene terminar el nivel con la observación que reordena todo lo anterior y que suele llegar tarde a quien aprende Kotlin por imitación. Ninguna de estas construcciones amplía lo que el lenguaje puede hacer. Todas son funciones ordinarias marcadas como insertables, escritas en unas pocas líneas de la librería estándar, que no generan ningún objeto y que producen un código equivalente al que produciría la versión escrita a mano con una variable local. Si su aportación fuera la capacidad, serían prescindibles; si su aportación fuera la brevedad, se juzgarían contando caracteres y ganaría siempre la más corta. Su aportación real es otra: son palabras de un vocabulario con el que se anota la intención de un bloque en el instante exacto en que el lector va a entrar en él, y esa anotación llega antes que el contenido, que es justamente cuando resulta útil. Por eso el uso correcto no se parece a una optimización sino a una decisión de redacción, y por eso los criterios que funcionan proceden de la escritura y no de la ingeniería: no repitas una palabra que no aporta, no prometas en el título algo que el párrafo no cumple, no encadenes tantas subordinadas que haya que releer la frase, y no uses una palabra culta solo porque existe. La consecuencia práctica es incómoda para quien busca una regla mecánica, y conviene aceptarla sin rodeos: no hay ninguna tabla que decida por ti, porque la información que estas funciones transmiten depende de qué sabe ya quien lee y de qué necesita saber a continuación, y eso solo se puede juzgar en contexto. Lo que sí hay es una prueba honesta que se aplica en segundos y que no admite autoengaño: tapa el bloque, lee el nombre, predice el contenido, destapa y comprueba. Un código donde esa predicción acierta siempre es un código que se puede leer en diagonal, y leer en diagonal con confianza es, en un proyecto de cierto tamaño, la diferencia entre poder mantenerlo y tener que reconstruirlo mentalmente cada vez.
- Aplica la prueba de tapar el bloque a diez usos de funciones de ámbito de tu proyecto y anota en cuántos falló tu predicción.
- Busca un anidamiento con dos parámetros implícitos y reescríbelo nombrándolos. Mide cuántas líneas creciste y decide si valió la pena.
- Localiza un
?.letcon un elvis de varias líneas y conviértelo enif. Justifica la elección hablando solo de altura visual de las ramas. - Redacta las cuatro convenciones de esta lección en el lenguaje de tu equipo y comprueba que cada una se puede verificar leyendo una sola expresión.
- Encuentra un bloque de ámbito que no quepa en la pantalla, extráelo a una función con nombre y compara qué comunica ahora la línea que quedó.