fun interface: un método abstracto, un nombre y una conversión
Marcar una interfaz con la palabra clave fun la convierte en interfaz funcional y habilita la conversión SAM: a partir de ese momento puedes pasar una lambda donde se espera el tipo. Parece azúcar y no lo es. Esta lección estudia qué exige exactamente el compilador para aceptar la marca, qué bytecode genera la conversión desde que se adoptó invokedynamic, y sobre todo el juicio que casi nadie hace bien: cuándo un tipo con nombre es mejor que un tipo función anónimo, incluyendo el argumento de rendimiento por evitar el boxing y el argumento contrario, decisivo, de que un parámetro de interfaz funcional no se puede insertar en línea.
Kotlin tiene dos maneras de expresar aquí va un comportamiento: el tipo función, que es anónimo, estructural y nativo del lenguaje, y la interfaz funcional, que es nominal, extensible y heredada de la tradición de la JVM. Durante años la segunda solo estuvo disponible para interfaces escritas en Java, porque el compilador aplicaba conversión SAM automática al interoperar y no a los tipos propios. La palabra clave fun delante de interface cerró esa asimetría y abrió una decisión de diseño que ahora hay que tomar conscientemente en cada API pública. No es una cuestión de gusto: las dos opciones se compilan de forma distinta, se comportan distinto ante la sobrecarga y ante los tipos primitivos, y una de ellas es incompatible con la optimización más importante que tiene Kotlin para funciones de orden superior.
- Declarar una interfaz funcional y conocer las condiciones exactas que impone el compilador.
- Explicar cómo se materializa una lambda convertida por SAM y por qué dejó de generarse una clase por cada una.
- Argumentar cuándo un tipo con nombre supera a un tipo función: sobrecarga, primitivos, interoperabilidad y miembros propios.
- Reconocer el caso en que el tipo función es obligatorio porque la interfaz funcional no puede insertarse en línea.
Un método abstracto y un nombre propio
Una interfaz funcional es una interfaz con exactamente un miembro abstracto, y ese miembro tiene que ser una función. Puede tener todos los métodos con cuerpo que quieras, un companion object y extensiones; lo que no puede tener es una segunda función abstracta ni una propiedad abstracta, porque una propiedad abstracta esconde uno o dos accesores abstractos y rompería la cuenta.
fun interface Validador {
fun valida(entrada: String): Boolean
fun y(otro: Validador) = Validador { valida(it) && otro.valida(it) }
companion object {
val NoVacio = Validador { it.isNotBlank() }
}
}
val correo = Validador { it.contains("@") }
val fuerte = Validador.NoVacio.y(correo)
Fíjate en lo que acaba de ocurrir dos veces: donde se espera un Validador has escrito una lambda. Eso es la conversión SAM, y solo funciona porque la interfaz está marcada con fun. Sin esa marca tendrías que escribir un object : Validador completo. Con interfaces declaradas en Java la conversión siempre ha sido automática, porque Java carece de tipos función y sin ella la interoperabilidad sería insoportable.
La conversión exige que el tipo esperado sea conocido en el punto de la expresión: funciona en un argumento, en una asignación con tipo declarado o en un retorno tipado, y no funciona si escribes la lambda suelta y esperas que el compilador adivine. También existe la referencia al constructor de la interfaz funcional, ::Validador, que produce una función capaz de envolver un tipo función en el tipo nominal; es la herramienta de migración cuando quieres sustituir un typealias sobre un tipo función por una interfaz de verdad sin romper a quien ya llamaba.
Los métodos con cuerpo, las propiedades implementadas, los miembros del companion object y las funciones de extensión declaradas fuera no cuentan para el límite. Solo hay una casilla abstracta y está reservada a una función. Tampoco cuentan los miembros heredados de Any, que es la razón de que una interfaz funcional pueda seguir siéndolo aunque redeclare equals o toString.
Que la casilla abstracta esté limitada a una función y no admita propiedades es una regla con motivo, no un descuido. Una propiedad abstracta esconde uno o dos accesores, así que aceptarla obligaría a definir a cuál de ellos corresponde la lambda y convertiría una regla trivial de contar en una casuística. Kotlin prefiere la regla trivial, y si necesitas un contrato con un dato y una operación, eso ya no es una interfaz funcional: es una interfaz normal, y esconderla tras una lambda solo confundiría a quien la lea.
Qué genera la conversión
Al principio, cada lambda convertida por SAM producía una clase anónima en el bytecode: un fichero más por cada punto de conversión y un objeto nuevo por cada evaluación. Desde Kotlin 1.5 el modo por defecto usa invokedynamic con la fábrica de lambdas de la plataforma, de modo que la clase se sintetiza en tiempo de ejecución la primera vez y las lambdas sin capturas se reutilizan como instancia única.
flowchart LR L[Lambda en el codigo fuente] --> C[Conversion SAM del compilador] C --> I[Instruccion invokedynamic] I --> F[Fabrica de lambdas de la plataforma] F --> O[Instancia del tipo funcional] O --> U[Uso como cualquier objeto]
La consecuencia práctica es que el argumento las interfaces funcionales generan basura de clases dejó de ser cierto hace muchas versiones. Lo que sigue siendo cierto es que la conversión produce un objeto, con su identidad y su ciclo de vida. Dos lambdas idénticas escritas en sitios distintos no son iguales entre sí: la igualdad de un Validador es la de Any, no la de su comportamiento, y eso importa cuando registras y das de baja escuchadores.
Fuera de la JVM el mecanismo cambia, pero el modelo semántico no: una interfaz funcional es un tipo nominal cuyas instancias pueden crearse con sintaxis de lambda.
Conviene además saber dónde no se aplica la conversión. No se aplica si el tipo esperado no está determinado, de modo que val v = { s: String -> s.isNotEmpty() } produce un tipo función y no un Validador por mucho que uno exista con esa forma. No se aplica sobre interfaces con dos miembros abstractos ni sobre interfaces normales sin la marca fun, donde tendrás que escribir el objeto anónimo completo. Y no se aplica hacia atrás: teniendo un Validador en la mano, pasarlo donde se espera (String) -> Boolean requiere una referencia al método, porque el tipado nominal no se convierte solo en estructural.
val v: Validador = Validador { it.isNotBlank() }
val f: (String) -> Boolean = v::valida // del nombre a la forma
val g: Validador = ::comprobar // de la funcion al nombre
fun comprobar(entrada: String) = entrada.length > 3
Cuándo el nombre gana
Hay cuatro razones sólidas para preferir una interfaz funcional a un tipo función, y ninguna es estética.
La primera es la sobrecarga. En la JVM, (String) -> Unit y (Int) -> Unit acaban siendo el mismo tipo tras el borrado, así que dos funciones que solo se distingan por eso chocan. Dos interfaces funcionales distintas nunca chocan, porque son tipos distintos también en el bytecode.
Esa colisión no es un caso raro: aparece en cuanto una API ofrece dos formas de configurar lo mismo, una con un valor y otra con un proveedor, o dos variantes de una operación que difieren solo en el tipo del parámetro devuelto. Con tipos función hay que renombrar una de las dos; con tipos nominales no hay nada que renombrar.
La segunda es el boxing. Un tipo función sobre primitivos se representa con el tipo genérico de la biblioteca, y los genéricos de la JVM no admiten primitivos: cada Int que atraviesa la frontera se envuelve. Una interfaz funcional con parámetros primitivos conserva el primitivo.
// Cada llamada envuelve el Int y desenvuelve el Boolean.
val caro: (Int) -> Boolean = { it % 2 == 0 }
// Ninguna envoltura: la firma usa el primitivo.
fun interface PredicadoInt {
fun test(valor: Int): Boolean
}
val barato = PredicadoInt { it % 2 == 0 }
La tercera es que un tipo con nombre puede llevar equipaje: métodos por defecto que componen, un companion object con instancias canónicas, documentación adherida al tipo y no al parámetro. La cuarta es la interoperabilidad: desde Java, un parámetro de tipo función aparece como el tipo genérico de la biblioteca de Kotlin, feo y con Unit de retorno explícito; una interfaz funcional aparece como lo que es.
Tipo función
Estructural, anónimo y componible. Es lo que quiere una API interna, y lo único que el modificador inline puede insertar en línea sin asignar ningún objeto.
typealias sobre tipo función
Da nombre al lector pero no al compilador. Mejora la documentación, no permite sobrecargar ni añadir miembros y desaparece por completo en el bytecode.
fun interface
Nominal, extensible y con miembros propios. Es lo que quiere una frontera pública, una jerarquía de combinadores o una firma sobre tipos primitivos.
Interfaz normal
Cuando el contrato tiene dos operaciones o quieres impedir que se cree al vuelo con una lambda. La ausencia de la marca fun también comunica algo.
Migrar sin romper a nadie
El caso práctico más frecuente no es elegir desde cero, sino descubrir a mitad de camino que un tipo función que empezó siendo un detalle interno se ha convertido en parte de la API pública. Kotlin permite hacer esa transición de forma casi indolora, y merece la pena conocer el orden de los pasos.
// Antes: nombre para el lector, ningún tipo nuevo.
typealias Transformador = (String) -> String
// Después: tipo real, con sitio para crecer.
fun interface Transformador {
fun aplicar(entrada: String): String
fun luego(otro: Transformador) =
Transformador { otro.aplicar(aplicar(it)) }
}
Toda llamada que pasaba una lambda sigue compilando gracias a la conversión SAM. Lo que deja de compilar es el código que trataba el valor como función y lo invocaba con paréntesis o lo componía con operaciones de tipos función; para esos sitios existen la referencia al método y la referencia al constructor de la interfaz funcional, que hacen de puente en ambas direcciones mientras dura la migración.
La lección de fondo es que la dirección fácil es de forma a nombre. Empezar con un tipo función y ascender a interfaz funcional cuando el concepto se consolida cuesta poco; empezar con una interfaz funcional y descubrir que hacía falta que fuese insertable en línea cuesta reescribir la firma y a todos sus llamadores. En la duda, y para código interno, el tipo función es la apuesta reversible.
El modificador inline solo puede insertar en línea parámetros cuyo tipo sea un tipo función. Si declaras el parámetro como interfaz funcional, la lambda se materializa en un objeto real, la llamada es virtual y pierdes además el retorno no local. En funciones de orden superior pequeñas y muy llamadas —el patrón de filter, map, use o cualquier envoltorio propio— el tipo función no es una alternativa: es la única opción que conserva la optimización.
Debajo de la discusión sintáctica hay una decisión de teoría de tipos que conviene nombrar, porque es la que explica todos los casos particulares sin tener que memorizarlos. Un tipo función es estructural: (String) -> Boolean no significa nada más que algo que toma un texto y devuelve un booleano, y por tanto cualquier cosa con esa forma encaja, venga de donde venga y signifique lo que signifique. Una interfaz funcional es nominal: un Validador y un Autorizador con la misma firma exacta son tipos incompatibles, y esa incompatibilidad, que a primera vista parece burocracia, es literalmente el servicio que estás comprando. El tipado estructural maximiza la reutilización y minimiza la ceremonia, y por eso es la elección correcta para las lambdas efímeras, los argumentos de una función interna, los bloques que se pasan a una función inline y todo aquello cuya vida entera cabe en la pantalla. El tipado nominal maximiza el significado y la evolución, y por eso es la elección correcta en las fronteras: en la API pública de una librería, en la firma que otro equipo va a leer sin contexto, en el punto donde quieres poder añadir dentro de dos versiones un método de composición o una instancia canónica sin obligar a nadie a cambiar su código. La pregunta operativa que resuelve el noventa por ciento de los casos es cuánto viaja ese comportamiento: si nace y muere dentro de una función, el tipo función; si cruza un módulo, aparece en documentación o va a ser implementado por gente que no escribió la firma, el nombre. Y hay un matiz que suele decidir los empates: un typealias sobre un tipo función da el nombre sin dar el tipo —mejora la lectura pero no crea una entidad nueva, no permite sobrecargar, no admite miembros y desaparece en el bytecode—, mientras que la interfaz funcional da el nombre y el tipo. Cuando dudes entre las tres, pregúntate si lo que quieres es que el compilador sepa la diferencia o solo que el lector la vea.
- Convierte un
typealiassobre un tipo función de tu código enfun interfacey comprueba cuántas llamadas siguen compilando sin cambios gracias a la conversión SAM. - Intenta declarar una interfaz funcional con dos funciones abstractas y luego con una propiedad abstracta. Anota los dos mensajes de error.
- Escribe la misma operación con
(Int) -> Booleany con una interfaz funcional de parámetro primitivo, ejecútalas millones de veces y mide la diferencia de asignaciones. - Declara una función
inlinecon un parámetro de interfaz funcional y observa la advertencia del compilador; cámbialo a tipo función y comprueba que ahora el retorno no local es legal. - Añade a tu interfaz funcional un método de composición y una instancia canónica en el
companion object, y reescribe con ellos una cadena de validaciones que antes hacías con lambdas sueltas.