APIs experimentales: `@RequiresOptIn` y el traslado de la responsabilidad
Entre lo estable y lo privado hay un tercer estado imprescindible para cualquier biblioteca que quiera aprender de sus usuarios antes de congelar un diseño. Esta lección explica cómo declarar un marcador con `@RequiresOptIn`, la diferencia entre consentir localmente y propagar el marcador, qué significa exactamente que el usuario asuma la responsabilidad de una firma que puede cambiar, y qué garantías ofrece la biblioteca estándar, incluida la política de soporte de seguridad de dieciocho meses vigente desde Kotlin 2.4.
Las cuatro lecciones anteriores construyen un régimen severo: todo lo público se declara, todo lo declarado se vuelca, todo lo volcado se vigila y todo lo que cambia recorre un calendario de retirada. Ese régimen es exactamente lo que hace falta para un contrato maduro, y es también una jaula si se aplica desde el primer día a un diseño que aún no se ha probado con nadie. El dilema es real y no tiene salida dentro de la dicotomía habitual: publicar algo estable sin haberlo validado condena a mantener durante años una firma equivocada, y mantenerlo privado hasta estar seguro impide obtener precisamente la validación que haría falta para estarlo. Kotlin resuelve la tensión con un tercer estado que no es ni estable ni oculto, sino ofrecido bajo consentimiento explícito: la biblioteca advierte que ese trozo puede cambiar, el consumidor firma que lo ha entendido, y el compilador guarda la firma.
- Declarar un marcador de aceptación explícita con
@RequiresOptIny aplicarlo al subconjunto de la API que aún puede cambiar. - Distinguir el consentimiento local del consentimiento propagado y elegir cuál corresponde en cada punto de una base de código.
- Razonar qué responsabilidad exacta se transfiere al consumidor y qué obligaciones deja en pie para quien publica.
- Situar las garantías de la biblioteca estándar, incluida la política de soporte de seguridad de dieciocho meses, dentro del mismo esquema.
Declarar el marcador y ofrecer el trozo inestable
El mecanismo tiene una pieza central que sorprende por su economía: el marcador no es una anotación predefinida sino una anotación propia que el autor de la biblioteca declara y que a su vez lleva @RequiresOptIn. Eso permite que cada biblioteca tenga tantos marcadores como áreas inestables, con mensajes y severidades distintas, y que el consumidor consienta con la granularidad que quiera en lugar de aceptar un bloque entero.
@RequiresOptIn(
message = "La API de lotes aun puede cambiar de firma sin previo aviso.",
level = RequiresOptIn.Level.ERROR
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
public annotation class RedExperimental
@RedExperimental
public fun conectarPorLotes(hosts: List<String>): List<Conexion> = TODO()
Que el marcador sea una anotación ordinaria tiene una segunda ventaja: se documenta, se enlaza y se busca como cualquier otro símbolo, de modo que un consumidor puede localizar de un vistazo todo lo que depende de esa decisión abierta sin recorrer la biblioteca entera.
Los dos detalles de la declaración importan más de lo que parecen. La retención binaria es necesaria para que la marca viaje dentro del artefacto y el compilador del consumidor pueda verla; con retención de solo fuente, la protección desaparece en cuanto la biblioteca se publica. Y el nivel decide si el uso sin consentimiento produce aviso o error: para un área que va a cambiar de verdad, el error es la elección honesta, porque el aviso se ignora y el consumidor acaba sorprendido.
Conviene además elegir con cuidado qué se marca. Marcar demasiado convierte el mecanismo en ruido y enseña al consumidor a consentir por reflejo; marcar demasiado poco reintroduce el problema original. La regla practicable es marcar aquello sobre lo que todavía queda una decisión de diseño abierta que se sabe formular en una frase, y desmarcarlo en cuanto esa frase tenga respuesta.
Un buen indicador de que el marcado está bien calibrado es la relación entre lo estable y lo experimental a lo largo del tiempo. En una biblioteca sana, cada versión mueve símbolos del segundo grupo al primero y el conjunto experimental se renueva con lo que se está diseñando ahora; si en cambio el conjunto solo crece, lo que hay no es un mecanismo de aprendizaje sino un vertedero, y los consumidores dejarán de distinguir entre lo que de verdad va a cambiar y lo que lleva tres años marcado por inercia.
Hay un caso intermedio que merece mención propia, porque aparece constantemente en jerarquías de tipos: una interfaz cuyo uso es perfectamente estable pero cuya implementación por terceros no lo es, porque la biblioteca prevé añadirle miembros. Para eso existe una anotación específica que exige consentimiento solo a quien herede o implemente, dejando libre a quien únicamente invoque.
@SubclassOptInRequired(RedExperimental::class)
public interface Transporte {
public fun enviar(carga: ByteArray)
}
Consentir localmente o propagar la marca
Del lado del consumidor hay dos gestos distintos y elegir mal entre ellos es el error más común de todo el mecanismo. El primero es consentir en el punto de uso con @OptIn, que aísla la decisión: la función que lo lleva puede invocar la API marcada, y sus propios llamantes no se enteran de nada. El segundo es anotar la propia declaración con el marcador, lo que propaga la exigencia hacia arriba y obliga a quien la use a consentir a su vez.
// Consentimiento local: la decision muere aqui
@OptIn(RedExperimental::class)
internal fun abrirTodas(hosts: List<String>): List<Conexion> =
conectarPorLotes(hosts)
// Consentimiento propagado: quien me llame tambien tendra que firmar
@RedExperimental
public fun abrirTodasPublica(hosts: List<String>): List<Conexion> =
conectarPorLotes(hosts)
El error habitual consiste en propagar por costumbre, anotando con el marcador cualquier función que toque algo experimental. Eso contamina hacia arriba una biblioteca entera a partir de una única dependencia interna, y convierte en experimental lo que en realidad era perfectamente estable.
El criterio para decidir es de contención. Si la inestabilidad de la API marcada puede quedar encapsulada dentro de tu implementación, de modo que un cambio de su firma se resuelva tocando solo tu fichero, el consentimiento local es correcto y probablemente obligatorio. Si en cambio el tipo inestable aparece en tu propia firma pública, encapsular es imposible y propagar es lo honesto, porque un cambio ajeno se convertirá inevitablemente en un cambio tuyo.
// Consentimiento a escala de modulo: comodo y peligroso a partes iguales
kotlin {
compilerOptions {
optIn.add("com.ejemplo.red.RedExperimental")
}
}
Hay un tercer gesto, menos conocido y muy útil en bibliotecas grandes, que consiste en consentir sobre la marca ajena y volver a marcar con la propia. Una biblioteca que construye sobre un área experimental de otra puede decidir que asume ese riesgo internamente pero que expone la funcionalidad bajo su propio marcador, con su propio mensaje y su propio calendario. Eso convierte una dependencia inestable en una promesa gestionada, que es exactamente el trabajo que un consumidor espera que hagas por él.
El consentimiento global del módulo tiene su lugar en aplicaciones finales, donde no hay nadie aguas abajo a quien proteger y donde la alternativa sería salpicar cientos de anotaciones. En una biblioteca que se publica, en cambio, es casi siempre un error, porque desactiva la señal en todo el módulo y hace desaparecer del código el rastro de qué depende de qué.
flowchart TD
A[Uso de una API marcada] --> B{El tipo inestable aparece en mi firma publica}
B -->|Si| C[Propaga: anota tu declaracion con el marcador]
B -->|No| D{Puedo absorber un cambio de firma en este fichero}
D -->|Si| E[Consiente localmente con OptIn]
D -->|No| C
C --> F[Tus consumidores heredan la advertencia]
E --> G[La inestabilidad queda contenida]Qué se transfiere y qué no
Conviene además dejar por escrito, en la documentación del proyecto, qué significa exactamente la marca en tu caso, porque el mecanismo es del lenguaje pero la promesa es tuya. Hay bibliotecas en las que experimental significa que la firma puede cambiar en cualquier versión menor, otras en las que significa que la implementación aún no es eficiente y otras en las que significa que la funcionalidad completa puede desaparecer. Las tres son políticas legítimas y radicalmente distintas en cuanto al riesgo que asume quien consiente.
La palabra que mejor describe el mecanismo es consentimiento informado, y conviene tomarse en serio las dos mitades. La transferencia es real: a partir del momento en que el consumidor escribe la anotación, ha declarado por escrito y de forma verificable que conoce el riesgo, de modo que si la firma cambia en una versión menor no hay ruptura de contrato, porque nunca hubo contrato. Esa es la razón por la que estas declaraciones pueden evolucionar libremente sin someterse al calendario de la lección anterior, y por la que el volcado de superficie puede configurarse para excluirlas.
Esa exención tiene una consecuencia concreta sobre la herramienta de la lección anterior, y conviene configurarla de forma deliberada en lugar de heredarla por descuido. El validador puede excluir del volcado todo lo anotado con un marcador determinado, lo que evita que la superficie experimental genere ruido en cada revisión; pero excluirla también significa perder la señal de cuánto está cambiando. La configuración que mejor funciona en la práctica es la intermedia: mantener el volcado de lo experimental en un fichero aparte, que se revisa con criterios distintos y sobre el que no rige el calendario de retirada.
apiValidation {
nonPublicMarkers.add("com.ejemplo.red.RedExperimental")
}
Lo que no se transfiere es igual de importante y suele olvidarse. Sigue siendo obligación de quien publica que el mensaje explique el riesgo concreto en lugar de repetir la palabra experimental; que el estado experimental tenga un final previsto y no se eternice durante años, que es la forma más habitual de convertir el mecanismo en una mentira; que los cambios se documenten en el registro aunque no fueran obligatorios; y que la marca se retire cuando el diseño se estabilice, porque un símbolo maduro que sigue marcado enseña a todo el mundo a desconfiar de las marcas.
Lo que promete la biblioteca estándar
El propio Kotlin usa este mecanismo sobre sí mismo, con marcadores como el de la API experimental de la biblioteca estándar o el de los tipos sin signo antes de su estabilización, y su comportamiento es el mejor modelo disponible de cómo se administra el tercer estado a gran escala: se marca, se recogen usos reales, se ajusta la firma dos o tres veces y se estabiliza, momento en el que la marca desaparece sin dejar deuda.
Ese ciclo deja una enseñanza que vale para cualquier proyecto: el estado experimental es útil precisamente porque termina. Un símbolo que entra marcado, recibe uso real, se ajusta un par de veces y sale estabilizado ha cumplido su función; uno que entra marcado y sigue igual cinco versiones después no está aprendiendo nada, simplemente se está eludiendo la decisión. Conviene revisar el conjunto marcado en cada versión mayor y forzar para cada elemento una de dos salidas: estabilizarlo o retirarlo.
Sobre lo estable, el compromiso es de otra naturaleza y desde Kotlin 2.4 está formulado con precisión: la biblioteca estándar cuenta con una política de soporte de seguridad de dieciocho meses, con parches retroportados a las líneas de versión activas. La consecuencia práctica para quien consume es que existe una ventana conocida durante la cual una versión ya publicada seguirá recibiendo correcciones de seguridad sin obligar a un salto de versión, lo que permite planificar las actualizaciones en lugar de reaccionar a ellas. Y la consecuencia para quien publica una biblioteca propia es que ahora existe una referencia concreta con la que alinear su propia promesa, en lugar de inventarla desde cero.
El marcador impide el uso accidental, no explica el riesgo. Un mensaje útil dice qué decisión sigue abierta y qué forma tendría el cambio: si la firma va a ganar un parámetro, si el tipo de retorno va a cambiar o si la función entera puede desaparecer. Con eso el consumidor puede calcular su exposición; sin eso, solo puede confiar.
Marcador propio
Cada área inestable declara su anotación con retención binaria. La granularidad fina es lo que hace útil el consentimiento.
Contener o propagar
Si el tipo inestable asoma en tu firma pública, propaga. Si puedes absorber el cambio en tu fichero, consiente localmente.
Ventana conocida
La política de dieciocho meses de la biblioteca estándar convierte la seguridad en algo planificable en lugar de reactivo.
Vale la pena reparar en lo insólito de lo que ocurre aquí, porque este mecanismo no encaja en ninguna de las categorías habituales de un lenguaje de programación. Los sistemas de tipos, tal como se enseñan, describen propiedades del programa: qué valores puede tomar una expresión, qué operaciones admite, qué no puede ser nulo. La aceptación explícita describe otra cosa completamente distinta, que es el grado de certeza que el autor de una biblioteca tiene sobre una decisión de diseño todavía no cerrada, y lo hace de un modo que el compilador puede verificar y que viaja dentro del artefacto compilado. Dicho de otro modo, Kotlin decidió que el estado epistémico de quien escribe la biblioteca es información de primera clase, tan digna de ser comprobada como un tipo, y que la forma correcta de manejar la incertidumbre no es esconderla ni disolverla en documentación que nadie lee, sino declararla y exigir un acuse de recibo. Es difícil exagerar lo sano que resulta ese movimiento para un ecosistema completo, porque ataca la patología más común de las bibliotecas jóvenes, que consiste en publicar como definitivo lo que solo es provisional y descubrirlo cuando ya hay diez mil proyectos dependiendo de ello. Si se junta esta pieza con las cuatro anteriores aparece por fin el diseño completo del nivel, y su coherencia es notable: el modo explícito obliga a decidir qué es contrato, el volcado lo materializa, la validación automática lo custodia, el ciclo de obsolescencia permite cambiarlo sin traicionarlo y la aceptación explícita permite además aprender antes de comprometerse. Las cinco piezas responden a la misma pregunta desde ángulos distintos, y esa pregunta, que es la pregunta central de este nivel y probablemente de todo el oficio de mantener software que otros usan, es cómo se puede seguir cambiando de opinión sin que cambiar de opinión le cueste el trabajo a otro.
- Declara un marcador propio con retención binaria y nivel de error, y aplícalo al área de tu API sobre la que aún queda una decisión abierta.
- Escribe el mensaje del marcador nombrando la decisión concreta y la forma que tendría el cambio, sin usar la palabra experimental como explicación.
- Consume esa API desde otro módulo dos veces, una conteniendo la inestabilidad con consentimiento local y otra propagando la marca, y compara qué ve el llamante en cada caso.
- Localiza una interfaz tuya cuyo uso sea estable pero cuya implementación no lo sea y protégela con la anotación de consentimiento para subtipos.
- Redacta la promesa de soporte de tu biblioteca tomando como referencia la política de dieciocho meses de la biblioteca estándar y comprueba si tu proceso de publicación puede sostenerla.