Kotlin, parte 3: Métodos de extensión y sobrecarga de operadores

Kotlin permite la sobrecarga de operadores, como Scala y C++, pero de manera controlada, como Groovy. Es decir, no se pueden definir operadores de manera arbitraria como en Scala, donde se puede definir un método ~->, pero sí se pueden tener métodos que se puedan invocar con operadores como +, -, *, [] etc.

Es curioso que siendo un lenguaje con tipado estático, no se fueron por el camino "limpio" para implementar esto, que era definir interfaces para los operadores (por ejemplo, Summable, o Plus, Minus, etc), sino que lo implementaron de igual manera que en Groovy, sólo que pues en Groovy funciona porque es un lenguaje dinámico. Esto presenta dos problemas: primero, que hay que saberse muy bien cuáles son los operadores que se pueden sobreescribir, junto con los nombres de los métodos correspondientes, los cuales no siempre son obvios a la hora de estar implementado uno (Para usar / ¿es divided, quotient, div o qué?) y el otro, que es más difícil saber si una clase tiene operadores sobrecargados o no, ya que hay que revisar los métodos que implementa, en vez de simplemente revisar las interfaces que implementa, y honestamente es más fácil simplemente hacer prueba y error (a ver si funciona si le pongo un +).

Ejemplos de sobrecarga:

class Sobrecargado(val x:Int) {
  operator fun plus(otro:Sobrecargado) = Sobrecargado(this.x + otro.x)
  operator fun minus(otro:Sobrecargado) = Sobrecargado(this.x-otro.x)
  operator fun times(otro:Sobrecargado) = Sobrecargado(this.x*otro.x)
  operator fun div(otro:Sobrecargado) = Sobrecargado(this.x/otro.x)
  operator fun invoke(s:String) {
      for (i in 1..x) {
          println("Imprime $i $s")
      }
  }
  override fun toString() = "Sobrecargado $x"
}

//Y así se puede usar
val s2 = Sobrecargado(2)
println(Sobrecargado(1)+s2)
println(Sobrecargado(5)-s2)
println(s2*Sobrecargado(3))
println(Sobrecargado(6)/s2)
s2("hola")

Noten como esa última llamada, s2("hola"), no es inmediatamente obvia. ¿No era s2 un objeto? ¿Cómo es que de repente se invoca como si fuera una función? Pues porque implementa el método invoke. La única diferencia con Groovy, es que todas estas llamadas, así como las implementaciones de los operadores, se verifican en tiempo de compilación, lo cual evita problemas en tiempo de ejecución, sin embargo no ayuda en la legibilidad y mantenibilidad de ese código, particularmente cuando se mezcla esto de la sobrecarga de operadores con la siguiente característica que explico a continuación...

Métodos de extensión

Kotlin permite agregar métodos a clases existentes. Esto es muy útil para cuestiones de metaprogramación, o simplemente para poder extender el comportamiento de clases existentes sin tener que usar herencia (lo cual sería inútil si no tenemos control sobre la creación de instancias de dichas clases).

Podemos agregar métodos a nuestras clases que hemos definido anteriormente, Persona y Sobrecargado:

fun Persona.edad() = Date().time-nacimiento.time
fun Sobrecargado.doble() = Sobrecargado(x*2)

//Incluso podemos agregarle cosas a clases cuyo fuente no tenemos
fun String.foo() = println("$this FOO!")

//Y así las usamos
println(Persona("Juan").edad())
"Probando".foo()
println(Sobrecargado(3).doble())

Esto puede ser muy conveniente, pero si se abusa de esta característica, se puede tener código difícil de mantener. Es posible definir operadores para extender una clase existente:

operator fun String.times(t:Int) {
  val sb = StringBuilder()
  for (i in 1..t) {
    sb.append(this)
  }
  return sb.toString()
}

Y entonces ya pueden hacer algo como println("-"*20) y les sale una línea de 20 guiones. Excepto que ese código no va a compilar, porque la inferencia de tipos global a veces falla (por eso no es bueno tener inferencia global, es mejor sólo a nivel local) y entonces tienen que definir ese método (operador, pues) como String.times(t:Int):String es decir, tienen que ponerle el tipo de retorno explícitamente. Esta falla en el compilador se debe a la complejidad del código: la combinación de método de extensión, operador sobrecargado (por la manera informal en que se implementan), y la inferencia a nivel global), el compilador erróneamente infiere que el método debe devolver void (bueno, en Kotlin le llaman Unit, igual que en Scala), cuando en realidad el método evidentemente debe devolver String (¿qué clase de operador devolvería void, cierto?)

Pero bueno, muy aparte de este problema con la inferencia, imaginen que un día se integran a un equipo que está trabajando en un proyecto en Kotlin, ya de varios KLOC, y de repente se topan con un println("+"*50). Es un poco obvio lo que debe hacer ese código. Pero, ¿qué tal si lo hace mal? Alguien nuevo en Kotlin podría suponer que el operador * fue sobrecargado en el código de String, o si es un método de extensión, pues que es algo incluido en el core del lenguaje. Y estarían equivocados. Y van a tener que buscar en dónde se encuentra definido el método de extensión String.times y la verdad es que sólo con ayuda del IDE lo van a lograr.

Sospecho que de hecho la manera en que Kotlin implementa los operadores es a propósito, para poder combinarlos con métodos de extensión, ya que si la sobrecarga fuera por medio de interfaces, no se podrían agregar operadores a clases existentes cuyo código fuente no se puede modificar (como clases del core del lenguaje, por ejemplo). Tengo sentimientos encontrados al respecto... esto me recuerda a las categorías de Objective-C, pero nuevamente estamos hablando de un lenguaje dinámico y aquí estamos tratando con tipado estático, que de repente tiene cosas como de lenguaje dinámico. Esto huele a que le quisieron dar la vuelta a limitaciones de su sistema de tipos, que es lo mismo que ha estado haciendo Java durante años. Podría decirse que Kotlin ya trae deuda técnica desde su nacimiento...

Conclusión

Es evidente que Kotlin tiene varias características que lo hacen más conveniente que Java; sin embargo, hay que tener algunas precauciones al comenzar a usarlo, no tanto por los errores obvios que van a tener al principio debido a las diferencias sintácticas, sino por los típicos errores de principiante de querer usar Kotlin como si fuera Java, o de querer usar todas las características que el lenguaje ofrece (solamente mencioné algunas que considero sobresalientes, hay muchas cosas más por explorar, pero eso ya será tarea de cada quien).

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de skuarch

gracias

excelente introduccion es bueno leer para saber de lo que habala uno