Avanzado
Mixins
Un mixin es una definición similar a la clase en el sentido en que define tanto comportamiento como estado, pero la intención es proveer una funcionalidad “abstracta” que puede ser incorporada en cualquier clase u objeto. De esta manera es una opción más flexible y favorece más la reutilización que la herencia basada en clases.
Algunas características de los mixins:
- no puede instanciarse (solo se instancian las clases)
- se “lineariza” en la jerarquía de clases para evitar el problema de los diamantes
- solo soporta herencia de otros mixines (un mixin no puede heredar de una clase, aunque una clase puede heredar de una superclase y opcionalmente de muchos mixines)
Otros detalles técnicos
- El proceso de mixing es estático
- no se puede descomponer los mixins de una clase
Mixin simple
Aquí hay un ejemplo del mixin más sencillo posible, que provee un método
Entonces se puede incorporar a una clase
Luego podemos usarlo en un programa / test / biblioteca
Mixin con estado
Además de comportamiento (métodos), un mixin puede definir atributos.
Y se puede usar así
Acceso a atributos
Los atributos declarados en un mixin pueden ser accedidas desde una clase que utilice dicho mixin.
Mezclando múltiples mixins
Una clase puede incorporar varios mixins a la vez.
La lista de mixins se debe separar con un “and”, indicando el mecanismo de linearización de izquierda a derecha. Esto implica que en este ejemplo:
Mixins abstractos
Un mixin puede ser abstracto si llama a un método que no tiene implementación. En ese caso se produce una restricción: la clase, objeto o mixin que quiera incorporar dicho mixin debe definir ese método.
Aquí vemos un ejemplo de un Mixin abstracto que provee capacidades de volar:
reduceEnergy(meters) es un método requerido.
Cuando se ejecuta “self.reduceEnergy()”, se enviará un mensaje al objeto actual (self). Esto implica que el método puede estar en cualquier parte de la jerarquía del objeto receptor, como previamente describimos.
Hay tres posibles casos:
Método implementado en una clase
En este caso la clase provee la implementación del método requerido:
Método implementado en una superclase
El método que el mixin requiere no está implementado en la clase que se mezcla con Flying sino en la superclase que debe definirse al final de la jerarquía:
Aquí vemos que al agregar el mixin solo se agregan las definiciones propias del mixin a la clase. El method lookup sigue partiendo desde el objeto receptor y subiendo a través de la jerarquía de clases.
Método definido en otro mixin
Veamos qué sucede si convertimos la clase Energy en un mixin:
Y la usamos de la siguiente manera:
En este caso Flying necesita el método reduceEnergy que no es implementada por la clase ni por sus superclases, pero sí en el otro mixin (Energy) que forma parte del conjunto de mixins que incorpora BirdWithEnergyThatFlies.
En este caso el orden en el que escribimos los mixins no es relevante, ya que al enviar un mensaje a self comenzamos la búsqueda a partir del objeto receptor que incorpora todas las definiciones de todos los mixins. Más adelante veremos que el orden de las declaraciones puede ser importante para el method lookup en otras variantes
Linearization
Los mixins se conectan a la jerarquía de clases mediante un algoritmo que se llama linearization. Esta es básicamente la misma forma en la que funcionan los traits / mixins de Scala (para profundizar al respecto puede verse este blog).
Este mecanismo “aplana” las relaciones entre clases y mixins, asegurando que solo quede una jerarquía lineal entre ellas. De esa manera la jerarquía queda como una lista ordenada para que el mecanismo de method lookup tenga un flujo predecible y el programador no tenga que tomar decisiones.
Aquí hay algunos ejemplos de “linearizations”:
La jerarquía de B es fácil de entender, porque queda en el mismo orden en que la escribimos:
Si agregamos un nuevo mixin:
La nueva jerarquía quedaría como
Como hemos visto en uno de los ejemplos anteriores, los mixins del lado derecho tienen menos precedencia en la jerarquía, ya que el method lookup se resuelve de izquierda a derecha.
La cadena de resolución para D queda
Redefinición de métodos
Entender el proceso de “linearization” es importante para implementar mixins en forma modular que colaboran sin conocerse entre ellos. A continuación presentaremos casos más complejos donde una clase o mixin redefine un método.
Clase que redefine un método de un mixin
La clase tiene prioridad por sobre los mixins dado que está a la izquierda en el algoritmo que lineariza los elementos, por lo tanto puede redefinir un método definido en un mixin.
Dado el siguiente mixin
Una clase puede incorporar el mixin Energy y redefinir el método “reduceEnergy(amount)“
Si ejecutamos en la consola
Veremos que la energía de pepita quedará con el mismo valor.
Llamada a super (en una clase que redefine un método de un mixin)
Como en cualquier otro método que redefine un método de una superclase, dentro del cuerpo podemos usar la palabra clave super para invocar al método original que estamos redefiniendo.
La misma ejecución en la consola produce el siguiente resultado:smile
Llamada a super en un mixin
Este es el caso más complejo (y también el más flexible). Un mixin puede redefinir comportamiento y también usar la implementación original. En ese caso la palabra super funciona como una especie de dynamic dispatch (si se nos permite la licencia).
Es complejo porque mirando solo la definición del mixin no podemos saber exactamente cuál es el código que estará ejecutando al invocar super().
Por ejemplo:
¿Dónde está la definición doFoo() que ejecutará super? No lo podemos saber en este contexto, debemos esperar a que una clase utilice M1 en su definición.
Dada esta clase
y esta definición
Esto implica tener esta jerarquía lineal:
Ahora sabemos que la llamada a “super” en el mixin M1 llamará al método “doFoo(chain)” definido en la clase C1 (la superclase de C2). Pero este es un caso particular, no podemos generalizar a todos los usos posibles de M1.
La forma de entender esto es que la jerarquía lineal se construye como hemos visto anteriormente, y entonces “super” implica encontrar la primera implementación existente a partir de la derecha del mixin donde estamos ubicados.
Repasando el ejemplo
Stackable Mixin Pattern
Si tenemos
- un conjunto de mixins que implementan un determinado comportamiento haciendo una llamada a super,
- y una clase que hereda ese mismo comportamiento de una superclase (sin llamar a super)
se produce entonces una situación llamada stackable mixin pattern. El efecto que tiene es que cada mixin se ubica como intermediario para “decorar” o agregar funcionalidad. “Stackable” implica que los mixins pueden apilarse o combinarse resultando en una implementación del chain of responsibility pattern.
Aquí vemos un ejemplo similar al anterior pero con algunos mixins adicionales
Y aquí tenemos las clases
Al ejecutar este código
se imprime por consola lo siguiente
es decir, la jerarquía lineal obtenida.
Mixins con Objetos
Los objetos autodefinidos (WKOs) también pueden combinarse con mixins
En este caso la cadena queda conformada de la siguiente manera:
Una vez más aplican las mismas reglas para la “linearization”, el objeto definido como ‘pepita’ tiene prioridad ya que está a la izquierda de la cadena, por lo tanto puede redefinir cualquier método.
Lo complementamos ahora con herencia de clases
Mixins en la instanciación
Los mixins pueden definirse en el momento que instanciamos un nuevo objeto. Esto nos permite una mayor flexibilidad, ya que no necesitamos crear una nueva clase que únicamente combine el conjunto de clases y mixins de una jerarquía. De esa manera evitamos la proliferación innecesaria de clases.
Aquí tenemos un ejemplo: dada la siguiente clase y el siguiente mixin
En lugar de crear una nueva clase para combinarlos…
podemos directamente instanciar un nuevo objeto que los combine
Las mismas reglas aplican a combinaciones de mixins.
Aquí tenemos un ejemplo un poco más complejo
Luego lo usamos de la siguiente manera
Inicialización de referencias en la linearización
El mecanismo de linearización permite inicializar las referencias de cada abstracción: solo debemos encargarnos de pasarle el valor en el lugar donde esté definida dicha referencia. En el ejemplo anterior:
Herencia de mixins
Es posible que un mixin tome definiciones de otro. En el mismo ejemplo anterior, podríamos pedir que el mixin GetsHurt herede de Energy:
El programa queda:
Limitaciones
Mixins de métodos nativos
No queda claro de qué manera los mixins pueden combinarse con clases nativas :^P
Redefiniendo comportamiento sobre un objeto anónimo
Podemos definir un objeto que linearice de mixines y clases y defina su propio comportamiento, como vemos en este ejemplo:
Type System
El sistema de tipos de Wollok (Wollok Type System) es una funcionalidad opcional, que por defecto está activado pero puede apagarse en la configuración. El sistema de tipos permite ocultar la información explícita de tipos, ya que para los desarrolladores novatos esto implica tener que explicar/entender un nuevo concepto.
Type System - Parte I
En un primer momento, se comienza a partir de un sistema de tipos que solo incluye objetos predefinidos, objetos definidos por el usuario y literales: números, strings, booleanos, listas, conjuntos, closures, entre otros.
Type System - Parte II
El sistema de tipos de Wollok permite combinar clases con objetos, chequear que los envíos de mensajes sean correctos y que los tipos en las asignaciones sean compatibles.
Todo esto sin necesidad de anotar ningún tipo en las definiciones de las variables, los parámetros o los valores devueltos por los métodos.
Por ejemplo
Estos son los tipos que infiere Wollok:
- pepita : se tipa a
Ave
- pato : se tipa a
AveQueNada
- mascota : se tipa a
Ave|Superman
, que significa que puede ser una instancia de Ave (o sus subclases) o de Superman. Esto es porque los mensajes que se le envían a la mascota, en este caso volar(), es implementado en dichas clases. Que en AveQueNada se redefina volar() no altera la inferencia.
Si en otra parte del código se asigna a la variable mascota un objeto de otra clase, se producirá una advertencia acerca de la inconsistencia de tipos de datos.
WollokDocs
Wollok tiene una sintaxis especial para documentar los elementos del lenguaje.
Por ejemplo, las clases:
Los métodos y las referencias (variables de instancia) también pueden tener este tipo de documentación. Esto facilita el entendimiento posterior para quien lo quiera usar después, ya que el IDE muestra esa documentación en los tooltips y otras partes visuales.
Mecanismo de excepciones
Wollok provee un mecanismo de excepciones similar al de Java / Python.
Una excepción representa una condición en la que no pueden continuar enviándose mensajes los objetos involucrados en resolver un requerimiento, por lo tanto tira una excepción.
Eventualmente en alguna otra parte un interesado podrá manejar esta situación para reintentar la operación, tratar de resolverlo, avisar al usuario, etc.
Así que estas son las dos operaciones básicas que pueden hacerse con una excepción:
- throw: “tirar” una excepción hasta que alguien la atrape
- catch: atrapar la excepción que se encuentra en la pila de ejecución, definiendo código para manejar esa excepción.
Throw
Veamos un código de ejemplo de la sentencia throw:
Aquí el método m1() siempre tira una excepción, que es una instancia de MyException.
Importante: solo puede hacerse throw de instancias que formen parte de la jerarquía de wollok.lang.Exception (esto es Exception o sus subclases).
Try-Catch
Aquí tenemos un ejemplo de cómo atrapar una excepción:
Este programa atrapa cualquier MyException que se tire dentro del código encerrado en el try.
Then Always
Además del bloque “catch”, un bloque “try” puede definir un bloque “always”, que siempre se ejecutará sin importar si hubo un error o no.
Catchs multiples
Un bloque try puede tener más de un catch, en caso de necesitar manejar diferentes tipos de excepción de distinta manera:
Sobrecarga de operadores
Dado que Wollok no exige definiciones de tipos al usuario, no es posible definir dos mensajes con igual cantidad de parámetros y diferente tipo:
Ambos mensajes serán ejecutados por el mismo método.
No obstante, sí es posible definir dos mensajes con diferente cantidad de argumentos:
Identidad vs Igualdad
Wollok sigue las convenciones de igualdad e identidad que tienen Java / Smalltalk. Por defecto, dos objetos son iguales si son el mismo objeto.
Pero para algunos objetos la igualdad está redefinida, por ejemplo dos strings son iguales si tienen los mismos caracteres:
El operador == es equivalente al mensaje equals:
Para saber si dos referencias apuntan al mismo objeto, se utiliza el operador ===
En general, hay objetos que representan valores: los números, los strings, los booleanos, y los value objects, a ellos se les suele redefinir el == / equals en base a su estado.
Para más información ver el paper de Wollok en el que se habla de igualdad e identidad entre otros conceptos.