wandres.dev
EL LENGUAJE · quince años y un compilador nuevo

La filosofía: pragmatismo sobre pureza

Qué características rechazó Kotlin deliberadamente, qué aceptó a pesar de su coste, y qué revela ese patrón de decisiones sobre la clase de lenguaje que es y sobre para quién fue diseñado.

⏱ 15 min

Un lenguaje se define tanto por lo que contiene como por lo que sus diseñadores se negaron a incluir teniendo la capacidad técnica de hacerlo. Kotlin es un caso de estudio especialmente nítido, porque sus rechazos están documentados y argumentados: casi siempre se descartó algo defendible en teoría porque su coste, medido en dificultad de lectura, de herramientas o de migración, superaba a su beneficio. Ese criterio tiene nombre y es el eje de esta lección.

🎯 Al terminar esta lección sabrás
  • Formular el criterio de decisión de Kotlin como una asignación de presupuesto de complejidad.
  • Enumerar características concretas que Kotlin rechazó y reconstruir el argumento de cada rechazo.
  • Analizar qué características aceptó a pesar de su coste y qué las justificó.
  • Deducir del patrón completo qué clase de lenguaje es Kotlin y para qué población fue diseñado.

El presupuesto de complejidad

La idea rectora es que la complejidad de un lenguaje es un recurso finito. Cada construcción que añades no solo cuesta implementarla: cuesta enseñarla, cuesta que las herramientas la entiendan, cuesta que interactúe correctamente con todas las demás, y sobre todo cuesta leerla en código escrito por otros. Un lenguaje con veinte mecanismos brillantes que se combinan sin restricción no es veinte veces más expresivo, es cuatrocientas veces más difícil de revisar.

Kotlin gasta su presupuesto con un criterio explícito y bastante estable: prefiere resolver el ochenta por ciento de los casos con un mecanismo que se lee de un vistazo, y dejar el veinte por ciento restante en manos de una vía de escape verbosa pero visible. La palabra que más se repite en sus decisiones de diseño no es «potente», es legible.

flowchart TD
A[Propuesta de caracteristica nueva] --> B[Resuelve un problema frecuente y real]
B -->|no| X[Rechazada]
B -->|si| C[Se puede leer sin conocer el contexto lejano]
C -->|no| X
C -->|si| D[El IDE puede analizarla en codigo incompleto]
D -->|no| X
D -->|si| E[Compatible con la interoperabilidad existente]
E -->|no| X
E -->|si| F[Entra como experimental]
style X fill:#f38ba8,color:#11111b
style F fill:#a6e3a1,color:#11111b

Lo que rechazó a propósito

🟣

Excepciones comprobadas

El rechazo más famoso. La evidencia acumulada en Java fue que obligan a declarar lo que nadie quiere declarar y producen bloques vacíos que tragan errores. Kotlin no las tiene: todas las excepciones son no comprobadas.

🟣

Conversiones numéricas implícitas

Un Int no se convierte solo en Long. Hay que llamar a la conversión explícita. Molesta al escribir y elimina una clase entera de errores silenciosos de precisión y de resolución de sobrecargas.

🟣

Tipos de orden superior

No puedes abstraer sobre el constructor de tipo, es decir, escribir código genérico sobre «cualquier contenedor». Se descartó porque complica la inferencia y los mensajes de error para beneficiar a una minoría de usuarios.

🟣

Parámetros implícitos al estilo de Scala

La resolución implícita global se rechazó por ilegible: obliga a buscar por todo el proyecto qué valor se está inyectando. Las funciones de extensión y los parámetros de contexto cubren el caso útil de forma acotada.

🟣

Operadores definidos por el usuario

No puedes inventar símbolos. Solo puedes dar significado a un conjunto cerrado de operadores mediante funciones con nombres fijos. Es una defensa directa contra los dialectos privados dentro de una misma empresa.

🟣

Notación monádica y macros

Ni azúcar sintáctico para encadenar efectos ni metaprogramación en el lenguaje. Para lo primero están las corrutinas; para lo segundo, complementos de compilador escritos aparte y de forma explícita.

Estos rechazos no son abstractos: se ven al escribir la primera línea.

val pequeno: Int = 42
// val grande: Long = pequeno        // no compila: nada se ensancha solo
val grande: Long = pequeno.toLong()  // la conversión es visible y buscable

// el ternario no existe porque if ya es una expresión
val etiqueta = if (grande > 100L) "alto" else "bajo"

// las clases son finales salvo que se declare lo contrario
class Servicio            // nadie puede heredar de esto
open class Extensible     // el permiso es explícito y deliberado

La clase final por defecto merece un comentario aparte, porque invierte la decisión de Java sobre una base empírica bien conocida: heredar de una clase que no fue diseñada para ser heredada crea un acoplamiento invisible con sus detalles internos, y el autor de la clase base descubre que ha roto a alguien cuando ya es tarde. Kotlin no prohíbe la herencia; obliga a que sea una decisión consciente del que escribe la base, no del que escribe la subclase.

Hay dos rechazos más que conviene mirar de cerca porque revelan el mismo criterio aplicado en dirección contraria a la intuición. Kotlin no tiene operador ternario, no porque sea complejo sino porque es innecesario: si if es una expresión, el ternario es un segundo mecanismo para lo mismo. Y Kotlin no tiene miembros estáticos: los sustituye por objetos acompañantes, que son objetos de verdad y por tanto pueden implementar interfaces y recibir extensiones. Ambos casos muestran que eliminar una construcción también puede ser una forma de ganar poder expresivo, no de perderlo.

Lo que aceptó, y a qué precio

El pragmatismo no es minimalismo. Kotlin aceptó cosas caras cuando el problema era suficientemente frecuente.

La nulabilidad en el sistema de tipos costó introducir los tipos plataforma en la frontera con Java, un compromiso teóricamente sucio. Se aceptó porque el error del que protege era el más común de la industria.

Las funciones de extensión rompen la expectativa de despacho dinámico: se resuelven de forma estática por el tipo declarado, no por el real. Es una trampa conocida para quien viene de la herencia clásica. Se aceptó porque permite extender APIs ajenas sin envolverlas, que era una necesidad diaria en la migración de IntelliJ.

Las corrutinas metieron una transformación profunda dentro del compilador, con el coste de que el mecanismo no es una biblioteca que puedas leer. Se aceptó porque la alternativa —tipos monádicos que contaminan todas las firmas— habría partido el ecosistema en dos.

Los receptores en las funciones literales, la base de los DSL como el de Gradle, aceptan que dentro de un bloque el significado de un nombre dependa del contexto. Es la mayor concesión de Kotlin contra su propio principio de legibilidad local, y por eso vino acompañada de la restricción que impide anidar receptores sin marcarlo: una anotación que hace que, dentro de un bloque anidado, el receptor exterior deje de ser visible de forma implícita. Sin esa red, un error de anidamiento produciría código que compila y hace algo distinto de lo que parece.

Fíjate en el patrón común de las cuatro aceptaciones: en todas, la característica cara vino acompañada de un mecanismo de contención. La nulabilidad trajo el tipo plataforma acotado a la frontera; las extensiones trajeron reglas de resolución explícitas; las corrutinas trajeron la concurrencia estructurada, que impide que una tarea sobreviva a su ámbito; los receptores trajeron la restricción de anidamiento. Kotlin no rechaza el poder: exige que venga con freno.

// una extensión se resuelve por el tipo declarado, no por el real
open class Base
class Derivada : Base()

fun Base.nombre() = "base"
fun Derivada.nombre() = "derivada"

val x: Base = Derivada()
val cual = x.nombre()   // "base": despacho estático, no polimórfico

Qué dice todo esto del lenguaje

El patrón, visto de golpe, es coherente: Kotlin optimiza la lectura por parte de un desconocido dentro de un equipo grande, sobre una base de código que vivirá una década y que nadie puede reescribir de golpe. No optimiza la brevedad máxima, ni la expresividad máxima, ni la elegancia teórica. Cada rechazo de esta lección tiene el mismo antagonista: la construcción que es maravillosa cuando la escribes y opaca cuando la heredas.

Eso explica también sus límites honestos. Si trabajas en teoría de tipos, en verificación formal o en abstracciones algebraicas profundas, Kotlin te va a resultar deliberadamente romo, y esa opinión es correcta: no fue diseñado para ti. Fue diseñado para el ingeniero que abre un archivo ajeno un martes por la tarde y necesita entenderlo sin abrir otros seis.

Hay una consecuencia práctica que conviene interiorizar antes de escribir código serio. Si el lenguaje optimiza la lectura por parte de un desconocido, entonces el estilo idiomático no es una cuestión de gusto sino la continuación de la misma política. Escribir Kotlin idiomático significa preferir la construcción que se entiende sin contexto lejano: nombrar los argumentos cuando la llamada es ambigua, usar clases selladas en lugar de banderas booleanas combinadas, no encadenar cinco operaciones en una línea que hay que leer tres veces, y reservar los DSL con receptor para los casos donde la estructura del dominio realmente lo justifique. Quien escribe Kotlin como si fuera un lenguaje de máxima concisión está usando el presupuesto de complejidad en la única dirección que el lenguaje decidió no financiar.

El pragmatismo también es una teoría, solo que sobre personas

Conviene desmontar la lectura perezosa de que «pragmático» signifique ateórico o poco riguroso. Rechazar los tipos de orden superior exige entender exactamente qué se pierde y qué cuesta la inferencia con ellos; implementar las corrutinas como transformación a estilo de paso de continuaciones exige tanta teoría como implementarlas como mónada, y bastante más ingeniería. La diferencia no está en el nivel de rigor sino en qué se está optimizando. Un lenguaje académico optimiza propiedades del programa: expresividad, corrección demostrable, composicionalidad. Kotlin optimiza propiedades del proceso humano que rodea al programa: cuánto tarda alguien nuevo en ser productivo, cuánto se lee mal en una revisión de código, cuántas migraciones puede sobrevivir la base sin reescribirse, si el editor puede autocompletar sobre una línea a medio escribir. Ambas son teorías legítimas y ambas tienen métricas, solo que las de la segunda son más incómodas de publicar. La prueba de que la apuesta funcionó no es estética: quince años después, con la 2.4 y un compilador reescrito de arriba abajo, el código de Kotlin 1.0 sigue compilando. Ese es exactamente el resultado que se optimizó, y no fue un accidente.

⚔️ Reconstruir un rechazo
  1. Elige una característica que Kotlin no tiene y escribe el argumento a favor de incluirla en su versión más fuerte y honesta posible.
  2. Escribe después el argumento en contra desde el criterio de esta lección: legibilidad para un desconocido, coste para las herramientas y compatibilidad.
  3. Reproduce el ejemplo del despacho estático de extensiones y explica en un comentario por qué el resultado sorprende y por qué es correcto.
  4. Localiza en tu código o en una biblioteca conocida un DSL con receptor y evalúa si su ganancia de legibilidad compensa la pérdida de contexto local.
  5. Compara el mismo problema concreto resuelto en Kotlin y en un lenguaje más purista, y clasifica cada diferencia como ganancia de expresividad o como coste de lectura.