lunes, 18 de marzo de 2013

Sobre el polimorfismo en clojure y la oop

(Este post nace como comentario a esta discusión acerca de la programación orientada a objetos y la funcional)

No es mi intención "combatir" ni "batir" a la oop, aunque es verdad que algunos defensores de la programación funcional, sobre todo los recién llegados a la misma tienen la típica actitud de los herejes conversos de atacar con ferocidad aquello que defendían. Sin embargo 20 años de hegemonía casi absoluta y un tanto arrogante en algunos momentos de la oop se merecen señalar, al menos durante una temporada, sus limitaciones.

La oop supuso un avance respecto a la programación estructurada aunque para "batirla" tuvo que hacerle concesiones. Por ejemplo es perfectamente posible escribir programación estructurada con java y me atrevería a decir que la gran mayoría del uso de java dentro del mundo enterprise tiene mucho (¿demasiado?) de estructurada.

De la misma manera la oop y la funcional pueden combinarse, siendo scala uno de los mejores ejemplos: para algunos sus características funcionales son las que precisamente le permiten superar esas concesiones a la programación estructurada y llevar la oo mas lejos que java. Por otro lado clojure también comparte algunos elementos (¿concesiones? :-P) que son claves en la oop y que os seran familiares.


Uno de estos elementos es el polimorfismo. Uno de los ejes de la defensa de la oop que no es en absoluto exclusiva de la misma aunque hay que reconocer que ha sido el primer paradigma que le ha dado una papel central y lo ha popularizado. Al igual que con la encapsulación son conceptos "ortogonales" a la oo: lenguajes no oo lo tienen, incluso lenguajes anteriores a la oop.

En el debate que originó este post se han tratado dos temas que tienen que ver directamente con el polimorfismo. El ejemplo trata sobre la "entidad" usuario y un método de expiracion que debe modificar el estado del usuario y guardar ese cambio de forma permanente:
  1. Una de las cuestiones es donde "vive" el código que expira al usuario (o tal vez cualquier cosa que se pueda expirar). Esto esta relacionado con el tema del "modelo anemico" y la tensión entre la programación estructurada y la oop. @Alfredocasado propone una solución dinámica en la que el código vive en un sitio pero se inyecta temporalmente al usuario ya que en determinados contextos ahí es donde parece (o se espera) que deba estar. @jmarranz prefiere una situación mas estática en la que es explicito en tiempo de compilación que código se esta usando. Segun su punto de vista ese código no debiera pertenecer al "espacio de nombres" del usuario, sino a otro que tome al usuario como parámetro. Un punto de vista más "estructural" pero también más funcional.
  2. Otra es como unificar y simplificar el espacio de nombres de las funciones/métodos para evitar cosas como write-to-db, write-to-file o peor, write-to-db(data,flag1,flag2,...) 
En cuanto al punto primero sin saber más del contexto de expire no se muy bien donde lo pondría. Tiendo a pensar que no en un módulo donde haya funciones que tengan que ver con el usuario sino en uno que tuviera que ver con el objetivo de expirar un usuario o cualquier otra cosa (si hay mas cosas que se puedan expirar) O sea en el módulo que se encargara de agrupar las funciones que actuan sobre "éso" donde que algo haya expirado o no tenga sentido.
En cuanto al segundo en clojure (y cualquier otro lenguaje funcional moderno) tienes varias opciones para hacer polimórfica una función: multimétodos y protocolos.

Multimétodos

Los multimétodos pueden ser polimórficos en función de cualquier combinación de cualquiera de los valores (no solo el tipo) de sus parámetros. Esta forma de polimorfismo es estrictamente mas genérica y mas poderosa tiene una serie de ventajas que la hacen más flexible que ciertas implementaciones del polimorfismo en la oop. En estas el dispatch se realiza solo en función del tipo (clase) del primer argumento del método que muchas veces es implicito (this). 

Se puede indicar cual es el método que se usa por defecto si no hay uno definido para unos valores particulares y cual es el preferido si existen dos definiciones de métodos que choquen.

Protocolos

Los protocolos son algo así como interfaces pero más flexibles: cualquier tipo puede extenderse hacia un protocolo estaticamente o al vuelo, incluso los que no han sido definidos por el programador. Los protocolos se añadieron en clojure porque los multimetodos eran mas lentos que el dispatch usando interfaces de la jvm y para que los recien llegados de la oop estuvieran mas comodos :-P

Referencias

Otra cuestión es que el código que llama a write tiene que tener un "writer" o "repositorio". Este puede estar definido estaticamente importando el módulo adecuado (o módulos si se quieren diferentes tipos) O también se le puede asociar dinamicamente al usuario:

O se pueden usar variables globales al módulo que pueden ser dinamicamente redefinidas:

Anda, ¿es que entonces se pueden modificar variables globales en clojure?. Pues sí, pero como comente anteriormente la mutación es por defecto segura concurrentemente. En el caso de las variables globales (o vars) estas pueden ser redefinidas por "binding" solo dentro del ámbito lexico de ese binding y dicha redinifición es local al thread que ejecuta ese bloque de código.
Y ya que nos ponemos y las funciones en clojure se guardan en variables globales, ¿por qué no modificar la funcion write misma? Pero eso tal vez en otro capitulo.

Nota: recientemente he leido en el blog de un importante miembro de la comunidad que el uso de variables globales dinámicas tiene una serie de limitaciones en cuanto a la concurrencia y otros aspectos. Para evitarlas recomienda siempre exponer una llamada con el recurso como parámetro para que el llamador tenga la opción de llamar de una u otra forma. Así la habia escrito yo aunque sin ser consciente de todas las limitaciones que lo hacen necesario.

7 comentarios:

  1. Creo que hay aquí un popurrí de conceptos un poco liados (supongo que habrá que ir a la discusión esa :P).

    Si yo te digo ¿cómo clasificarías N "cosas"?.

    ¡Pues depende de que "cosas" sean! dirás. Y tendrás razón.

    Pues lo mismo con tu usuario y tu código.

    Un OOP "puro" te dirá que el usuario y su código van juntos, pero un TDD "puro" te dirá que los datos van por un lado, los interfaces van por otro y las implementaciones por otro distinto, todo ello con un buen aderezo de inyección (o a mano...) de dependencias (una para cada interface referenciado).

    Últimamente con Haskell, me doy cuenta que la OOP ha seguido un camino muy largo para terminar haciendo lo que se hace en Haskell (y otros lenguajes supongo, pero ese es del que he visto un poquito).

    Si no eres radical, la respuesta a tu pregunta NO EXISTE (de ninguna manera) hasta que no concretes el problema.

    De ahí tu duda:

    "En cuanto al punto primero sin saber más del contexto de expire no se muy bien donde lo pondría."

    ResponderEliminar
    Respuestas
    1. Mmmm tal vez si que he puesto varios temas y no he hecho o no he expuesto correctamente el analisis. Por un lado esta el tema de los modulos (o espacio de nombres), por otro el del polimorfismo y por otro el que he llamado "referencias" que serian como esos modulos se relacionan (en tiempo de compilacion y/o ejecucion). No se si tu analisis del tema tendria otra organizacion...
      Luego esta el tema del ejemplo que he continuado de los comentarios del debate en javahispano y que es un poco etereo. Tal vez habria que haberlo concretado mas.

      Eliminar
    2. No entiendo que es eso de "un TDD" puro, pero te puedo asegurar que TDD no tiene nada que ver con separar los datos del comportamiento o definir interfaces para todo. Me da la sensación de que estes referiendo más a la tipica arquitectura "spring" con interfaces para todo y clases anemicas. Esto no tiene nada que ver con TDD, de echo yo te diria que es lo más alejado posible a practicar buen TDD.




      Eliminar
  2. A mí, como me puede el pragmatismo, todas estas cosas me suenan a "pajas mentales" si no hay unos objetivos/resultados mesurables.

    Me explico, "si hago X, entonces modificar el código en ejecución es posible por que si no, no me lo permite". Objetivo mesurable, comprobable si se puede conseguir o no... luego probablement será en el precio a pagar por conseguirlo donde entrará el área subjetiva de si merece la pena o no.

    Pero lo que quiero decir es que discutir si algo es más funcional o más oop o más X como si eso fuera inherentemente bueno me parece enfangarse en discusiones filosóficas.

    Lo que hacemos no es poesía, no es arte por amor al arte.

    ResponderEliminar
  3. Gracias javier por el post, muy currado y muy interesante.

    La verdad es que la técnica del polimorfismo con los "multimetodos" me parece mucho más natural si pensamos en funciones, lo de los protocolos me gusta bastante menos la verdad.

    ResponderEliminar
  4. Javier te pierde el marketing de clojurelandia :)

    "Esta forma de polimorfismo es estrictamente mas genérica y mas poderosa que la de la oop"

    A ver la OOP es poco más que herencia, encapsulación y polimorfismo, pero polimorfismo considerado a través de la herencia de clases (o herencia de clases de interfaces si consideramos una interface un tipo de clase límite sin estado ni comportamiento), ni siquiera la **sobrecarga de métodos** se considera un pilar de la OOP, es una funcionalidad simpática (tampoco la ostia de útil a decir verdad) que hasta el propio C hubiera podido tener sin esfuerzo alguno (seguro que ya lo tiene no lo sigo).

    No digo que la sobrecarga de métodos de Clojure no sea cojonuda pero algunas cosas ya están inventadas hace un porrón de años por ejemplo lo de los parámetros por defecto lleva en C++ décadas y le guste o no a Smalltalk, C++ es el lenguaje de referencia clásico a la hora de hablar de OO, ni siquiera Java.

    Que Clojure tiene aspectos más avanzados que Java, sin duda alguna, pero es que CUALQUIER lenguaje hoy día es más avanzado que Java, lo que pasa es que no se si por manías, no se si por ego, existe cierta costumbre de "cagarla" en muchos lenguajes "modernos":

    C# : la ostia de avanzado, pero ligado al ecosistema Microsoft/Windows por mucho que algunos hayan intentado que no sea así.

    Groovy: la ostia de avanzado, pero sin tipado estático, apto para audaces, no apto para inútiles como yo que pasan más tiempo refactorizando, navegando y revisando código.

    Python, Ruby y similares hierbas dinámicas demasiado alucinógenas para mi :(

    Clojure: la ostia de avanzado, pero vaya la OOP es mala, para alguien que usa, seguro que malamente, la herencia a saco (y por tanto el polimorfismo) pues me deja en pelotas, y eso por no hablar del tema de la gestión de estado, que mi cabeza todavía no da para tanto (aunque después de entender la tail call optimization tengo esperanzas).

    Al final sólo me queda quedarme con mi pobre Java esperando que Oracle se apiade por fin de él, o Scala que aunque tengo la sensación que está diseñado por un psicópata tiene similares psicopatías de las mias (necesito que me encajen las cosas en la cabeza, no soy masoquista).

    Al final el que realmente manda es mi querido Google en Android y como Google dice que Java pues Java y yo tan obediente... a dios.

    Cambiando de tema:

    Vale Cloure es lo más de lo más, y viniendo el tema de alguien tan sabio como tú (y lo digo en serio) pues me ofrece respeto, pero en mi opinión hay que guardar distancias o al final acaba uno defendiendo como un idiota los intereses de otros.

    A ver, qué narices le va interesar decir a

    Rich Hickey, Stuart Halloway a Stuart Sierra y demás primos de Relevance Inc

    http://clojure.com/about.html
    http://thinkrelevance.com/team

    Pues que la OOP se inventó en el infierno y que Java es el peor lenguaje jamás inventado y sus primos no puramente funcionales siguen el mismo camino al infierno induciendo a las peores prácticas del universo :)

    ResponderEliminar
  5. Con un poco de retraso me gustaria hacer un par de apuntes:

    Como ves he corregido lo de "es estrictamente mas poderosa.." ya que efectivamente era una frase demasiado arrogante para ser valida tecnicamente. El polimorfismo en la oop basada en clases tiene una serie de caracteristicas que hace que sea util en contextos en el que el polimorfismo "ad-hoc" de clojure no lo es.

    Sin embargo tengo que aclarar que, aunque similar a la sobrecarga de metodos en java es mas util ya que no esta limitada a la clase (y su jerarquia) donde se define el metodo. Tu puedes definir un multimetodo en un modulo de una libreria y basar el api en el y el cliente en cualquier otro modulo no relacionado con el anterior puede extender ese multimetodo consiguiendo que ese api se adapte a sus necesidades sin ninguna clase de herencia.

    Aparte tenemos el mecanismo de los protocolos que es mas similar a la oop aunque sin la posibilidad de extender clases sino solo "interfaces" sin implementacion. En algun otro blog[1] me sume contigo en defenderla (con matices) pero para mi no es imprescindible si el lenguaje me da otros mecanismos de reusar codigo.

    [1] http://eamodeorubio.wordpress.com/2012/02/09/extends-es-una-mierda-no-te-habias-enterado/

    ResponderEliminar