Cómo se decide una característica: leer el proceso KEEP como se lee una constitución
Detrás de cada palabra clave de Kotlin hay un documento, una discusión pública de meses o años y a menudo un rechazo previo. Esta lección enseña a leer el proceso KEEP como fuente primaria: qué contiene una propuesta, qué significan sus estados, cómo se recorre el camino de experimental a estable a través de las etapas de previsualización, qué papel juega el comité de lenguaje ante un cambio incompatible, y por qué el argumento más frecuente en el diseño de Kotlin no es a favor de una característica sino en contra de todas las demás que esa característica haría imposibles.
La documentación de un lenguaje te dice qué hace. El proceso de evolución te dice por qué hace eso y no otra cosa, qué se descartó por el camino y qué precio se pagó por lo que quedó. En Kotlin ese proceso tiene nombre, repositorio público y un archivo entero de propuestas: el KEEP, siglas de Kotlin Evolution and Enhancement Process. Leerlo cambia la relación con el lenguaje, porque deja de parecer un conjunto de decisiones tomadas por alguien en otra parte y se convierte en un registro de compromisos discutidos, muchos de ellos difíciles, casi todos con las objeciones escritas al lado. Quien quiera opinar seriamente sobre Kotlin —o, más modestamente, entender por qué le falta algo que en otro lenguaje tiene— tiene que aprender a leer ese registro.
- Describir la anatomía de una propuesta
KEEPy el ciclo de vida completo de una característica hasta su estabilización. - Distinguir los niveles de madurez de la API y del lenguaje, y qué garantías concretas ofrece cada uno.
- Reconstruir un caso real de rediseño: el camino de los receptores de contexto a los parámetros de contexto.
- Argumentar por qué determinadas características aparentemente obvias han sido rechazadas de forma deliberada y sostenida.
Qué es exactamente una propuesta y qué contiene
Una propuesta KEEP es un documento en texto plano que vive en un repositorio público y que, en su forma madura, contiene siempre las mismas partes: una motivación que describe el problema en términos de código real que hoy se escribe mal, una propuesta de sintaxis y semántica, una sección de casos de uso, una discusión explícita de alternativas consideradas —esta es la parte que más se aprende leyendo—, un apartado sobre compatibilidad hacia atrás y otro sobre el impacto en las herramientas y en el código ya escrito. Esa estructura no es decorativa. Cada apartado corresponde a una objeción que históricamente ha hundido propuestas, de modo que el formulario es, en realidad, una lista de comprobación de las formas conocidas de equivocarse al diseñar un lenguaje. Alrededor del documento vive la discusión: un hilo público donde se plantean objeciones, y un rastro de incidencias en YouTrack donde se registran los problemas concretos que van apareciendo durante la implementación.
Hay una segunda vía de entrada que conviene conocer porque es la que usa casi todo el mundo sin saberlo. Antes de que exista una propuesta suele existir una incidencia en el sistema de seguimiento, abierta por alguien que tropezó con una limitación concreta escribiendo código normal. Esas incidencias se acumulan, se enlazan entre sí y en algún momento alguien observa que veinte de ellas son la misma pregunta formulada de veinte maneras. Ese es el instante en que nace una propuesta, y por eso el mejor servicio que puede prestar alguien que programa a diario no es proponer sintaxis sino reportar bien el problema: qué querías escribir, qué tuviste que escribir en su lugar y qué se rompió al hacerlo.
Lo que distingue a este proceso de un simple buzón de sugerencias es que la carga de la prueba está invertida. Una propuesta no tiene que demostrar que sería útil, cosa que casi cualquier característica puede demostrar; tiene que demostrar que su utilidad supera el coste permanente de existir. Ese coste incluye la complejidad añadida al modelo mental de todos los usuarios presentes y futuros, la interacción con cada característica ya existente, el trabajo de mantenimiento en cuatro backends, la carga sobre las herramientas y, sobre todo, el cierre de puertas: cada sintaxis que se ocupa es una sintaxis que ya no está disponible para nada mejor que pueda venir después.
Ese último punto merece subrayarse porque es el menos intuitivo. La gramática de un lenguaje es un recurso finito: hay un número limitado de símbolos, de posiciones y de formas que resultan legibles, y gastarlos en algo mediocre tiene un coste que solo se descubre años después, cuando aparece una idea mejor que habría encajado justo ahí y ya no cabe. Buena parte de las discusiones que parecen puramente estéticas en una propuesta —qué símbolo usar, si va antes o después, si necesita palabra clave— son en realidad discusiones sobre presupuesto.
flowchart LR IDEA[Idea o incidencia en YouTrack] --> PROP[Propuesta KEEP publicada] PROP --> DISC[Discusion publica y revision del diseno] DISC --> PROTO[Prototipo tras una bandera del compilador] PROTO --> EXP[Experimental con adhesion explicita] EXP --> PREV[Previsualizacion en una version del lenguaje] PREV --> EST[Estable con garantia de compatibilidad] DISC --> RECH[Rechazada o aplazada con motivos escritos] PROTO --> RECH
Conviene fijarse en que hay dos flechas que llegan al rechazo y en que una de ellas sale del prototipo. Eso significa que una propuesta puede morir después de estar implementada y funcionando, cosa que en la mayoría de los proyectos resultaría impensable porque el esfuerzo invertido se convierte en un argumento a favor de conservarlo. Aquí el esfuerzo invertido no cuenta como argumento, y esa disciplina es lo que impide que el lenguaje acumule características que nadie defendería si hubiera que proponerlas hoy desde cero.
En casi cualquier propuesta madura, la parte que más enseña no es la solución elegida sino el listado de las que se descartaron con el motivo al lado. Ahí está condensada la teoría de diseño del lenguaje, y ahí se aprende a distinguir una objeción real de una preferencia estética disfrazada de argumento técnico.
El camino de experimental a estable, y qué garantiza cada escalón
Kotlin no publica características terminadas de golpe. Las hace pasar por escalones con garantías crecientes, y entender esos escalones es lo que separa a un equipo que adopta novedades con criterio de uno que se lleva sustos. En el nivel más bajo, una característica vive detrás de una bandera del compilador y puede cambiar de sintaxis entre versiones menores sin previo aviso. Después pasa a un estado experimental en el que ya existe públicamente pero exige adhesión explícita: quien la use está firmando que acepta el riesgo, y esa firma es visible en el código y auditable en una revisión.
// Adhesion explicita: el uso queda marcado en el codigo, no escondido.
@OptIn(ExperimentalStdlibApi::class)
fun analizar(datos: ByteArray): String = datos.toHexString()
// Marcar una API propia como experimental: mismo mecanismo, tu propio marcador.
@RequiresOptIn(message = "API interna en evaluacion", level = RequiresOptIn.Level.ERROR)
annotation class ApiEnEvaluacion
Ese mecanismo de adhesión explícita no es solo para la biblioteca estándar: está pensado para que cualquiera lo use en su propio código, y es una de las herramientas de diseño más infrautilizadas del lenguaje. Un equipo que publica un módulo interno puede marcar la parte todavía en evaluación con su propio marcador y obtener exactamente la misma propiedad que obtiene el equipo de Kotlin: que el uso de algo inestable sea visible en el punto de uso, contable con una búsqueda y discutible en una revisión, en lugar de invisible hasta el día de la migración.
El escalón siguiente es la previsualización asociada a una versión del lenguaje, que permite compilar el código de mañana con el compilador de hoy para probarlo de verdad en proyectos reales antes de que las garantías se congelen. Y por encima está la estabilidad, que en Kotlin significa algo muy concreto y muy caro de sostener: compatibilidad de fuente y compatibilidad binaria dentro de la misma rama, y la promesa de que si algo tiene que romperse se romperá con un ciclo de deprecación que empieza en aviso, sigue en error y termina en ocultación, con varias versiones de margen y con migraciones automáticas siempre que sea posible. Cuando un cambio afecta a las garantías del lenguaje, no lo decide solo el equipo que lo implementa: existe un comité de lenguaje dentro de la fundación cuya función es justamente proteger a quienes ya escribieron código.
La etiqueta habla del contrato, no de la calidad. Una API experimental puede estar perfectamente implementada y ser rapidísima; lo que dice la etiqueta es que su forma todavía puede cambiar. Confundir ambas cosas lleva a dos errores simétricos: descartar herramientas excelentes por miedo, y meter en la base de una biblioteca pública algo cuya firma no está congelada.
Un caso real de principio a fin: de receptores a parámetros de contexto
El mejor ejercicio para entender el proceso es seguir una característica que no salió bien a la primera. Los receptores de contexto se propusieron para resolver un problema viejo y muy concreto: hay funciones que solo tienen sentido cuando están disponibles ciertas capacidades del entorno —un registrador, una transacción, un ámbito de corrutina, un formateador— y hasta entonces esas capacidades había que pasarlas como parámetro en cada llamada, ensuciando cada firma, o esconderlas en estado global, perdiendo trazabilidad. La primera propuesta permitía que una función declarase varios receptores implícitos a la vez.
El prototipo se publicó, la comunidad lo usó, y con el uso aparecieron los problemas: la resolución se volvía difícil de predecir cuando varios receptores ofrecían miembros con el mismo nombre, el código resultante era difícil de leer para quien llegaba nuevo porque no había forma de saber de dónde venía cada símbolo, y la interacción con las funciones de extensión producía casos que ni siquiera los autores sabían explicar rápido. En lugar de estabilizar lo que ya funcionaba a medias, la propuesta se retiró y se rediseñó: los contextos pasaron a ser parámetros con nombre en lugar de receptores anónimos, lo que devuelve la legibilidad y hace la resolución explicable, a cambio de un poco más de ceremonia. Esa segunda versión es la que se estabilizó en la versión 2.4 publicada en junio de 2026.
// Un contexto con nombre: se sabe de donde viene cada capacidad.
context(registro: Registrador)
fun transferir(origen: Cuenta, destino: Cuenta, importe: Dinero) {
registro.info("transferencia iniciada")
origen.retirar(importe)
destino.ingresar(importe)
}
// El llamante aporta el contexto una vez, no en cada firma intermedia.
with(RegistradorConsola()) {
transferir(a, b, Dinero(100))
}
Compárese con las dos alternativas que el diseño desplaza. La primera es pasar la capacidad como parámetro ordinario, que funciona y es explícita, pero contamina cada firma de la cadena de llamadas incluida la de las funciones intermedias que no usan la capacidad para nada y solo la transportan. La segunda es un estado global accesible desde cualquier sitio, que no contamina ninguna firma y a cambio destruye la trazabilidad y hace que las pruebas necesiten coordinación. Los parámetros de contexto ocupan exactamente el hueco entre ambas: la capacidad se declara donde se usa, se aporta donde se sabe y no aparece en medio.
La lección no es que el diseño original fuera malo, sino que el proceso tenía un mecanismo para descubrir que lo era antes de que fuera irreversible, y la voluntad de usarlo. Un lenguaje que estabiliza todo lo que prototipa acumula deuda para siempre; uno que retira propuestas maduras acepta pagar años de trabajo a cambio de no cargar a millones de personas con una decisión mediocre.
Hay un segundo aprendizaje escondido en este caso y es sobre el orden de las preguntas. La discusión no giró en ningún momento sobre si la característica era útil, cosa que estaba clara desde el primer día porque el problema era real y todo el mundo lo había sufrido. Giró sobre si la forma propuesta era explicable: si alguien que abre un fichero por primera vez puede saber de dónde viene cada símbolo sin ejecutar mentalmente el algoritmo de resolución. Cuando la respuesta resultó ser que no, ninguna cantidad de utilidad compensó, y la solución fue exactamente la que devuelve la explicabilidad al precio de escribir un nombre.
Cuando algo tiene que cambiar, no se borra. Se marca como obsoleto con un aviso, se acompaña de una sustitución automática siempre que sea posible, en una versión posterior el aviso pasa a error, y solo mucho después desaparece de la resolución. Ese calendario forma parte de la propuesta desde el principio, no es una consideración posterior de mantenimiento.
// La deprecacion como contrato, con la migracion incluida en la anotacion.
@Deprecated(
message = "Usa transferir con el nuevo parametro de contexto",
replaceWith = ReplaceWith("transferir(origen, destino, importe)"),
level = DeprecationLevel.WARNING
)
fun transferirAntiguo(origen: Cuenta, destino: Cuenta, importe: Dinero) { }
Por qué Kotlin dice que no a cosas que parecen obvias
Excepciones comprobadas
Rechazadas por experiencia acumulada: en la práctica producen capturas vacías y firmas que mienten. El diseño elige que el manejo de errores sea una decisión de la persona que llama, no una obligación sintáctica.
Operador ternario
Innecesario, porque if ya es una expresión. Añadirlo daría dos formas de escribir lo mismo, y la regla no escrita del lenguaje es que dos formas equivalentes son una forma de más.
Conversiones numericas implicitas
Rechazadas porque el ahorro de teclas no compensa la clase entera de errores silenciosos de precisión y de desbordamiento que introducirían en código que nadie revisa dos veces.
Implicitos al estilo Scala
La potencia es enorme y el coste de lectura también. Kotlin prefiere mecanismos acotados y visibles, y los parámetros de contexto son exactamente la versión disciplinada de esa idea.
La lista se puede alargar y todas las entradas cuentan la misma historia. No hay una palabra reservada para los miembros estáticos porque los objetos acompañantes cubren el caso con un mecanismo que además es un objeto de verdad, capaz de implementar interfaces y de recibir extensiones. No hay una visibilidad equivalente a la de paquete de Java porque los paquetes de Kotlin no son unidades de encapsulación y añadirla habría exigido cambiar qué significa un paquete. No hay macros porque la generación de código mediante procesadores resuelve la mayoría de los casos con un modelo mucho más simple de depurar, y porque un sistema de macros convierte cada base de código en un dialecto. En todos los casos el rechazo viene acompañado de una alternativa que ya existe, y esa es la señal de que la decisión estaba tomada desde el diseño y no desde la falta de tiempo.
Hay un patrón común en casi todos los rechazos, y conviene enunciarlo porque es transferible al diseño de cualquier API propia: Kotlin protege sistemáticamente la capacidad de leer código ajeno por encima de la comodidad de escribir código propio. Cada vez que una propuesta hace más rápido escribir a costa de hacer más difícil saber qué hace un fragmento sin abrir otros ficheros, la propuesta pierde. Ese sesgo explica el rechazo de los implícitos no marcados, explica que las extensiones se resuelvan estáticamente en lugar de participar en el despacho dinámico, explica que la herencia esté cerrada por defecto y explica que la nulabilidad tenga que declararse.
El sesgo tiene un límite y también conviene nombrarlo, porque de lo contrario se convierte en una excusa para no añadir nada nunca. Kotlin sí acepta pagar complejidad cuando la característica elimina una clase entera de errores o cuando desbloquea algo que no se podía expresar de ninguna forma razonable. La nulabilidad en el tipo es cara de aprender y se aceptó. Las corrutinas añadieron un modelo mental completo y se aceptaron. Los genéricos con varianza declarada son de las partes más difíciles del lenguaje y están ahí. El criterio, entonces, no es la simplicidad a toda costa sino una relación: cuánta complejidad se añade al modelo mental de todos frente a cuántos errores se vuelven imposibles para todos.
Muchas propuestas no se rechazan sino que se aparcan a la espera de otra pieza que todavía no existe: una capacidad de la plataforma, otra característica del lenguaje o simplemente más experiencia de uso. Consultar el estado antes de dar algo por descartado evita repetir discusiones que ya se tuvieron y que quizá terminaron en un aplazamiento y no en un no.
Hay una asimetría brutal en la evolución de un lenguaje de programación que casi nunca se explica y que, una vez entendida, reorganiza por completo la forma de juzgar cualquier propuesta, incluidas las propias. Añadir una característica a una biblioteca es reversible: si sale mal, se deprecia, se sustituye y en dos años nadie se acuerda, porque el usuario podía haber escrito esa función a mano y el lenguaje no cambió de forma. Añadir una característica a la gramática es irreversible en cualquier plazo humano, porque a partir del día en que se estabiliza existe código escrito con ella que hay que seguir compilando, existen herramientas que la entienden, existen libros que la enseñan y existe una generación de programadores para quienes es parte de lo que Kotlin significa. De esa asimetría se derivan casi todas las conductas del proceso que a un observador externo le parecen exceso de lentitud o de burocracia: la insistencia en escribir la sección de alternativas antes que la de implementación, la costumbre de mantener años un prototipo tras una bandera en lugar de estabilizarlo cuando ya funciona, la existencia de un comité cuya función explícita es defender a quienes no están en la discusión porque están ocupados manteniendo código antiguo, y la disposición a retirar una propuesta entera después de haberla implementado, como ocurrió con los receptores de contexto. La consecuencia práctica más importante para quien programa es que se puede leer el proceso al revés: cada característica que Kotlin tiene está ahí porque alguien demostró que no bastaba con una biblioteca, y cada característica que no tiene está ausente porque alguien demostró lo contrario o porque nadie ha demostrado todavía lo primero. Cuando eches en falta algo, esa es la pregunta que hay que hacerse antes de cualquier otra, y es exactamente la misma pregunta que deberías hacerte antes de añadir una abstracción al núcleo de tu propio sistema, porque el núcleo de tu sistema es la gramática de tu equipo y tiene la misma asimetría: lo que metes ahí no se saca.
- Elige una característica de Kotlin que uses a diario y localiza su propuesta original. Lee solo la sección de alternativas consideradas y escribe cuál de ellas habrías elegido tú y por qué.
- Sigue el rastro completo de los parámetros de contexto: propuesta inicial, discusión, retirada, rediseño y estabilización. Resume en cinco líneas qué información nueva hizo cambiar de opinión al equipo.
- Busca una propuesta rechazada que te parezca claramente buena. Escribe el mejor argumento en contra que encuentres en la discusión y decide honestamente si te convence.
- Marca una API interna de tu proyecto con tu propio marcador de adhesión explícita y comprueba cuántos usos aparecen. Ese número es tu deuda de estabilidad.
- Redacta una propuesta de una página para tu propio equipo siguiendo la estructura del proceso: motivación, diseño, alternativas, compatibilidad e impacto en las herramientas. Si la sección de alternativas queda vacía, todavía no has diseñado nada.