domingo, 11 de noviembre de 2012

Lo que amo de Scala: los tuples

Tengo una relación de amor y odio con los tuples. Los adoro cuando escribo código por primera vez y los odio cuando vuelvo a leer el código más tarde.  Son, por lejos, lo que me hace más difícil entender mi propio código.  Igual los uso a cada rato porque son demasiado prácticos.

En Scala, Java y cualquier lenguaje tipeado es fácil agregar un parámetro de entrada a un método.  Se define el nuevo parámetro y se deja el IDE o el compilador detectar todos los usos del método que fueron quebrados.  Los tuples me permiten exactamente lo mismo pero con el parámetro de salida.

Como ejemplo, 2 versiones de un método supuestamente complejo: evaluación de riesgo de un cliente  La primera versión solamente retorna un boolean que indica si el cliente es demasiado riesgoso
En la siguiente versión, el método calcula el riesgo usando varios criterios y se requiere retornar un mensaje que indica el porque.

Si no existiesen los tuples, tendría que definir una clase para contener el retorno.  A pesar que en Scala, crear un "case class" es rápido, tengo que por lo menos escoger un nombre para la clase.  Y, es en este preciso momento que adoro los tuples: cuando estoy pensando en un algoritmo no trivial mi capacidad creativa para nombrar las clases, objetos y variables tiende a cero.  Si tengo que detener mi pensamiento para nombrar correctamente la clase, evaluar si puedo re-utilizarla en otra situación, si debo hacerla más genérica, etc, pierdo completamente el hilo del algoritmo que estoy resolviendo.  Los tuples son una forma rápida de salirme del paso y enfocarme en la parte "matemática" del problema.

Los tuples son perfectos cuando uso el resultado del método localmente. Los problemas de mantenebilidad surgen cuando tengo que guardar los tuples en alguna colección y procesarla en otra parte del código. Cuando eso ocurre, tengo que hacer un refactoring y por eso los odio.


viernes, 2 de noviembre de 2012

Lo que amo de Scala: el poder (casi siempre) fácil de usar

Supongo que crear un lenguaje poderoso y al mismo tiempo fácil de usar, es una tarea compleja.  Cuando los creadores de Scala tienen que eligir, prefieren priorizar el poder.  Yo me alegro por eso.

Estoy consciente que la estrategia de Scala no es una necesariamente ganadora.   Se puede argumentar que parte del éxito de Java es debido a su simplicidad inicial.  Muchos competidores de Scala (como Ceylon y Kotlin), usan el argumento de "Scala es complejo" para tratar de ganarle terreno: básicamente, tratan de describir Scala como un lenguaje académico que contiene montones de ensayos que no se juntan perfectamente. Y Scala se ha ganado una imagen de elitista que primero busca satisfacer a los mejores programadores, como los creadores de framework, en detrimento del programador promedio.

Para mi no hay duda, yo prefiero el "poder" y estoy dispuesto a invertir un poco más de tiempo para manejarlo en forma responsable.  Voy a usar para argumentar, el "operator overloading", la implementación de la programación genérica y las conversiones implícitas.  Estas 3 técnicas son generalmente reconocidas como peligrosas o complejas.  Ademas puedo usar Java como referencia para argumentar.


Operator overloading

Java escogió no soportar "operator overloading".  El argumento inicial era que varias bibliotecas C++ (lenguaje donde existe esta capacidad) habían abusado tanto, que son responsables de fuentes que parecen una sopa de símbolos.  Java decidió proteger a los programadores de si mismo.

Yo siento que Java me trata como un bruto que, a pesar de estar informado de los potenciales riesgos, se va a dedicar generar código incomprehensible por el puro hecho que lo puede hacer.  Me parece que es mucho mejor permitir el "operator overloading" e implementar las bibliotecas básicas del lenguaje como una muestra de las buenas practicas.  Y Scala cumple con eso.

El primer ejemplo es la API colección, donde se usa "::" para agregar un elemento a una lista.  


El mismo ejemplo en Java



Otro ejemplo es la "combinator libary" que permite crear un DSL externo.

Una vez que uno se acostumbra a como funciona esta biblioteca, la interpretación de los símbolos "|", "~", "~>", "<~" y "^^" es inmediata y la sintaxis del DSL salta a la vista.

Entonces, ahora cuando veo un símbolo que no entiendo, yo se que no es el símbolo en si el problema, sino que me falta entender bien el concepto o la abstracción que representa.  Una vez que leí la documentación y luego de un poco de practica, el símbolo llega a ser casi invisible y me deja ver el código.  Al contrario, una palabra siempre atraerá la atención y ofuscara el código.  Entonces, luego de una pequeña curva de aprendizaje, el código queda mucho mas limpio y fácil de entender si se usan símbolos adecuadamente.

En este caso, el poder y la simplicidad de uso van de la mano: no hay ninguna razón para no incluir el operator overloading.

Nota: cabe señalar que Scala no tiene "operator overloading" en si, sino que es muy liberal en como nombrar las funciones y métodos.


Programación genérica y herencia

Java decidió no implementar la programación genérica hasta la versión 1.5.  No obstante tomo una decisión clave en su versión 1.0 con respecto a la herencia y los arreglos: si un String es un Object entonces, un arreglo de String es también un arreglo de Object.  Intuitivo pero incorrecto.


La implementaciòn de la programación genérica en Java desde la versión 1.5, no sigue el mismo patrón que los arreglos.  Una decisión que parecía intuitiva y simple, se transformo en una complicación: un arreglo es lógicamente una colección, pero en Java no lo es.

Scala en comparación decidió implementar los conceptos de covariance y contravariance.  Algunas veces, el compilador parece reclamar demasiado y prohibir código "correcto".   Hay que leer y entender la justificación de porque se comporta así, lo que hace el aprendizaje un poco más largo.

En mi caso, cuando me toco implementar un algoritmo de la teoría de los grafos donde los vértices y nodos eran genéricos, recupere el tiempo invertido.


Implicit conversion

Scala usa inplicit en varias partes.  Me voy a referir a las conversiones implícitas porque se parecen al autoboxing de Java el cual convierte un tipo primitivo a un objeto que lo encapsula, como por ejemplo un int a un Integer.


El poder de las conversiones implícitas de Scala no esta en duda.

No voy a olvidar cuando vi algo como el ejemplo anterior: primero pensé que Scala estaba imitando la sintaxis de Visual Basic.  No me gusto mucho la idea.  Luego descubrí que "to" en "0 to 10" es un método común y corriente pero no esta en la clase Scala.Int sino que en  la clase RichInt.  Este método retorna un rango sobre el cual itera la sintaxis "for".  Para que funcione, tiene que haber una conversión automática desde el Scala.Int 0 a RichInt.  Para eso, existen las conversiones implícitas 

Como en el autoboxing de Java, el peligro es que algo que se ve de un cierto tipo se comporta como otro al momento de ejecutar.

Ademas, Scala permite al programador definir sus propias conversiones implícitas.y obliga importar la definición del implicit para usarla.  Eso rompe muchas veces el "Google-copy-paste" porque es frecuente encontrar micro-ejemplos sin los import necesarios y copiar un código que no compila por una razón misteriosa.  Este ultimo problema se ha mitigado gracias a mejores mensajes del compilador (aunque a veces muy largos) y el mejor soporte en IDE.

Finalmente, para demostrar el super poder de las conversiones implícitas  es bueno mirar su uso en internal DSL como camel Scala DSL y compararlo con el camel Java DSL


Conclusión

En los tres casos mencionados anteriormente, el poder de Scala implica una curva de aprendizaje.  En caso de "operator overloading" se recupera inmediatamente y rápidamente con las conversiones implícitas, (basta usar una API diseñada como internal DSL),  En caso de la programación genérica me demando implementar un algoritmo no trivial para darme cuenta que Scala estaba en lo correcto.  Y fuera de estos 3 ejemplos, no he encontrado todavía alguna característica del lenguaje cuya curva de aprendizaje sea mayor al poder que otorga.