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:
- 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. - 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 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).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; Se define en un modulo una funcion polimorfica en funcion del tipo | |
;; de los *dos argumentos* | |
(ns modulo1) | |
(defmulti write (fn [writer data] [(type writer) (:type data)]) | |
;; En otro modulo que importa la definicion anterior generica de write | |
(ns modulo2 (require modulo1)) | |
(defmethod modulo1/write [java.io.FileWriter :user] [writer user] | |
(comment codigo especifico usando el FileWriter y teniendo en cuenta | |
que los datos son del tipo :usuario)) | |
(defmethod modulo1/write [java.io.CharArrayWriter :user] [writer user] | |
(.write writer (str user))) | |
(def writer (java.io.CharArrayWriter.)) | |
(modulo1/write writer {:name "jmarranz" :type "user"}) | |
(str writer) | |
;; "{:name "jmarranz" :type "user"}" | |
(defmethod modulo1/write [java.io.FileWriter :session] [writer session] | |
(comment otro codigo que usa FileWriter pero especifico para | |
el tipo :session)) | |
;; En otro modulo | |
(ns modulo3 (use modulo1)) | |
(defmethod write [SQLWriter :user] [w u] | |
(comment codigo que escribe los datos del usuario en la db)) |
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns modulo1) | |
(defprotocol Repository (write [this data]) (read [this key])) | |
(ns modulo2 (require modulo1)) | |
(extend-type javax.servlet.HttpSession | |
modulo1/Repository | |
(write [this data] (.setAttribute this (:id data) data)) | |
(read [this id] (.getAttribute this id))) | |
(modulo1/write my-http-session user) | |
(ns modulo3 (require modulo1)) | |
(extend-type java.io.CharArrayWriter | |
modulo1/Repository | |
(write [this data] (.write this (str data))) | |
(read [this id] (str this))) | |
;; Tambien se pueden definir tipos que pueden extender un protocolo | |
(deftype MyRepository [campo1 campo2] | |
Repository (write [this data] ...) (read [this id] ...)) | |
(modulo1/write (->MyRepository x y) user) | |
;; ¿que pasa si en otro modulo se crea un protocolo que define | |
;; una funcion con el mismo nombre? | |
(ns modulo4) | |
(defprotocol Writer (write [this data])) | |
(extend-type java.io.CharArrayWriter | |
Writer | |
(write [this data] (.write this (str data)))) | |
(ns modulo5 (require modulo1 modulo4)) | |
(def writer (java.io.CharArrayWriter.)) | |
;; Cada write esta dentro del espacio de nombres del modulo que | |
;; ha definido el protocolo | |
(modulo1/write writer data) | |
(modulo4/write writer data) |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; Asociamos a un usuario un repositorio | |
(assoc user :repository mi-db-writer) | |
;; luego en otro modulo | |
(defn expire [user] | |
(write (:repository user) | |
(assoc user :status :expired))) | |
;; Tambien se puede añadir a los metadatos del usuario | |
;; si no se quiere mezclar con los datos de negocio | |
;; Lo importante es que dos elementos que solo difieren | |
;; en los metadatos son *iguales* | |
(def yo-con-repo ^{:repository my-repo} | |
{:name "jneira" :type "user"}) | |
(def yo {:name "jneira" :type "user"}) | |
(assert (= yo yo-con-repo)) | |
;; expire quedaria asi | |
(defn expire [user] | |
(write (:repository (meta user)) | |
(assoc user :status :expired))) |
O se pueden usar variables globales al módulo que pueden ser dinamicamente redefinidas:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns modulo1) | |
(def ^:dynamic *writer* default-writer) | |
(defn expire | |
([user] (expire user *writer*) | |
([user writer] (write writer (assoc user :status :expired))) | |
;; En otro modulo que queremos usar la funcion expire con otro writer | |
(ns modulo2 (require modulo1)) | |
(binding [modulo1/*writer* my-session-writer] | |
(modulo1/expire user) | |
(modulo1/otra-funcion-que-usa-writer data)) |
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.