Agregando skills modulares

Agregando skills modulares

Presentamos un nuevo enfoque a la hora de añadir caracteristicas y habilidades a nuestras clases de manera modular y funcional.

Continuando y con el propósito de mejora el anterior post, Mutar de manera inmutable, desarrollabamos una primera idea para mutar de manera inmutable, en este siguiente artículo os intentaré hablar y desarrollar una forma en la que podemos aproximar está capacidad que hemos añadido pero esta vez desde un punto de vista puramente de la programación funcional.

¿Por qué digo desde un punto de vista puramente funcional? Al comienzo del anterior post comienzo comienzo hablando que uno de los target iba a ser evitar diseños de patrones como el de cake pattern. Tal y como  se quedó el código, era precisamente un diseño de cake pattern, dónde si se realizaba un pequeño cambio en uno de los traits padres, se tenia que retocar en cada una de las clases hijas que lo extendiera.

Desarrollando un poco mas esta idea, al final del post haciamos una mixtura entre el trait ConfigTypes y CanCopy[T], extendiendo la capacidad de ser copiable y consolidandose en un sistemas de jerarquías propiamente de la programación orientada a objetos. Donde por ejemplo, en el caso de ConfigA heredaba la capacidad de su padre concreto CanCopy[ConfigA].

portada-website-blog

El resultado

Esto no sólo hace que ambos traits estén fuertemente ligados, lo que se traduce a una fuerte dependencia y lo que se traduce (como comentaba al comienzo) a que cualquier cambio de la herencia padre CanCopy se tenga que ver reflejado en sus hijos. Sino que además, semánticamente hablando, ser parte de ConfigTypes no implica necesariamente o no es equivalente a ser copiable o tener la habilidad de poder mutarse, una cosa no es intrínseca a otra. De hecho es un plus que nos daria el gusto poder darle.
 
Esto es como si por ejemplo pensaramos que un ordenador tuviera que llevar tarjeta gráfica dedicada por default, o que un iPhone viniera con cargador en su caja. Obviamente habría que comprar la tarjeta gráfica y el cargador, respectivamente, y añadirlo o conectarlo de alguna forma para darle ese plus a nuestro objeto base.
 
Pero es que todavía más, el hecho de estar todo tan mezclado y cada vez más, nos frena a querer modularizar ambos conceptos, lo que se traduce a ser difícil de escalar el módulo de CanCopy y/o el módulo de ConfigTypes.

Advertencia

Nivel de fumada alta. El contenido de este post trae razonamientos psycho loco con un nivel más o menos alto y que, aunque se intenta desarrollar y desmenuzar todo lo posible, puede llegar a partir cerebros.

Contenido

Inyectar dependencias

Inyectar dependencia es una forma de modelar comportamientos de manera funcional, evitando a toda costa generar un complejo sistemas de herencias. ¿Esto cómo se hace? Sencillo, aislaremos la funcionalidad o habilidad, definiremos ahi sólo y exclusivamente los métodos pensados para cumplir esos comportamientos que son propios de dicha habilidad que queremos incluir, haciendo que cuando se adquiera se comporte como se espera.

En este nuestro caso, que la habilidad de poder copiarse tenga el método copy() que proporciona el comportamiento de copiarse a dicha habilidad.

Dicho de manera general, debemos proporcionarle la habilidad al comportamiento para que se comporte como tal.

Al final, esto no es más que un ejercicio de separar la función con la parte que usa la función. Por ejemplo, la habilidad de saltar requiere la función de contraer los músculos. Entonces si una persona necesita poder saltar, debemos definir la habilidad de saltar incluyendo en ella la funcion de contraer los musculos que hace que salte como en teoría debe ocurrir la acción.

Pongamos los pies sobre el código

Actualmente nos quedamos en el siguiente punto:

Como vemos, la capacidad de copiarse está asignada como una capacidad intrínseca, ligada y fuertemente dependiente de la propia entidad ConfigA (ó ConfigB, ó configC).

Esto precisamente es lo que NO queremos, entonces vamos a hacerle un par de ajustes: Lo primero demos un paso atrás y rompamos esa herencia infernal entre ConfigTypes y CanCopy.

Como ya no va a formar parte de ningun tipo de jerarquia de padre-hijos, no tiene sentido parametrizar CanCopy a tipos que extiendan de sí mismos. ¿Por qué? Precisamente porque ya no se va a extender nunca más de CanCopy. Además, esto estaba para restringir a que sólo «heredara» la capacidad de copiarse los tipos ConfigTypes. Ahora lo queremos generalizar.

Entonces, ahora vamos a hacer eso de proporcionar la habilidad al comportamiento acorde con el comportamiento que en teoría deberia ocurrir para el ente al que le queremos agregar la habilidad de copiarse.

Si lo que queremos es que ConfigC tenga la habilidad de copiarse, debemos definir un mecanismo de copiado acorde con lo que ConfigC se espera que haga cuando se copie.

Así entendemos que el comportamiento de ConfigC para copiarse es una instancia como tal del comportamiento de copiarse para el tipo ConfigC, a la cual se le ha provisto del mecanismo de copiarse de la forma como se espera que un objeto de tipo ConfigC se copie.

Y ahora sí entonces, que cualquier instancia de ConfigC tenga la habilidad de copiarse es precisamente que use la habilidad cuyo comportamiento pueda darle capacidad de copiarse, es decir, que use aquella instancia CanCopy[ConfigC] (CanCopyde tipo ConfigC) mediante el método copy().

Comentario

Pfff.. y pensar que esto lo he escrito el mismo día que me pincharon la segunda vacuna del Covid19…

Así entendemos que el comportamiento de ConfigC para copiarse es una instancia como tal del comportamiento de copiarse para el tipo ConfigC, a la cual se le ha provisto del mecanismo de copiarse de la forma como se espera que un objeto de tipo ConfigC se copie.

Y ahora sí entonces, que cualquier instancia de ConfigC tenga la habilidad de copiarse es precisamente que use la habilidad cuyo comportamiento pueda darle capacidad de copiarse, es decir, que use una instancia CanCopy[ConfigC] (CanCopyde tipo ConfigC) mediante el método copy().

Entonces, y ya por cerrar el razonamiento, toda lógica que haga uso o que implique tener que copiarse, va a necesitar usar esa instancia. Por tanto, desde ahora y con el objeto de suavizar el lenguaje, definiremos estas instancias como skills, más concretamente skills de ConfigC.

Con esto, la forma en la que llamabamos el método copy de un objeto de tipo ConfigC, ha cambiado ligeramente, requiriendo por parámetro la skill. El resto es prácticamente igual.

Pfff… ¿Y hay que pasarle la skill CADA VEZ que quiera llamar al método de copiar? Vaya verborrea… Pues sí y no.

Hasta ahora es cuando hemos conseguido nuestro objetivo final. Dotar de la capacidad de copiarse y de manera modular, evitando patrones de tarta. Aquí ya acabaríamos. Sin embargo, no vamos a dejarlo así como así, todavía queda un toque que hará que nuestra implementación sea algo mucho más elegante: (Ring,.. ring…) Si? Implicitos al habla…

Skills (implícitos)

A ver, esto es como la vida misma: ¿Acaso cuando vas a saltar gritas o comentas que vas a saltar? ¿O le hablas a los musculos diciendoles que necesitas saltar y que contraigan los músculos? Claro que no.. (vaya, espero que no).

En efecto, de manera natural uno tiene naturalizado (valga la redundacia) el acto de saltar, y simplemente lo hace porque se realiza la contracción de los músculos y demás historias de manera implícita en tu cuerpo.

Pues aquí ocurre lo mismo, no le pasamos por parametro de manera explicita la skill, sino que la alojamos en el oscuro universo de los implicitos, y esta se cuela sin previo aviso por las llamadas implícitas que hagamos, cual acechador nocturno esperando ansioso en las sombras.

Buah.. ahora sí. Ahora podemos tener algo super bonito y elegante como teniamos antes, pero con el bonus de ser una skill modular.

Conclusión

En este post, hemos conseguido darle una vueltita a lo que desarrollamos en el post de mutar de manera inmutable:

Agregar skills …

En este caso, nos hemos abstraido al concepto de añadir  skills o capacidades a nuestras clases, es decir, hemos aprendido no solo atribuirle la forma de copiarse a una clase, sino que hemos alcanzado el nivel super saiyan 2 de como atribuir cualquier mecanismo o skill.

… de manera modular …

Pero no sólo hemos conseguido eso, sino que, lo hemos hecho de manera modular, evitando enredos en las herencias, que implican que un mínimo cambio en el padre, se desarrolle, como una bola de nieve que crece, en muchos cambios en cada uno de los hijos que lo heredan. O sea, super saiyan 3.

… y parametrizada

Y además, por ser modular y parametrizado por el propio tipo, es decir, que no es un CanCopy para todos, sino es un CanCopy de tipo T. Podemos definir distintos comportamientos de copy para distintos tipos (ConfigA, ConfigB, etc.) pero que la semantica del comportamiento como tal sea la misma. Super saiyan 4 bro…

Es decir, que la idea de copiarse el objeto ConfigX sea la de copiarse, pero internamente cada cual haga sus movidas que necesite para que ConfigX se copie de manera correcta o esperada al tipo ConfigX sin afectar a la forma en la que se copia o deberia copiarse el tipo ConfigA, o el tipo ConfigC:

Mutar de manera inmutable

Mutar de manera inmutable

En Scala, a la hora de trabajar con objetos, puede ser normal tener la necesidad, y junto a esta necesidad, la posibilidad o la capacidad ligada de modificar o editar una propiedad intrínseca o un atributo propio de ese objeto, o incluso el mismo objeto en sí tras haberlo instanciado; consiguiendo realizar una serie de transformaciones más o menos secuencial de alguna forma más o menos sencilla e intituiva.

Sin embargo, aunque fuera natural tener esa necesidad, no es tan trivial tener esa posibilidad, al menos si se quiere hacer de manera correcta.

Lo cierto es, que para clases ya predefinidas, Scala suele disponibilizar un mecansimo más o menos inmediato para hacer esto. Pero, ¿Qué pasa para aquellos objetos que son instancias de nuestras propias clases? ¿Tenemos ese mecanismo? Si es así, ¿Modifica lo que queremos que modifique de la forma en la que nos gustaria que lo hiciera? O por el contrario, debemos incluirlo nosotros mismos. ¿De qué forma se incluye esto?

Contenido

Variables mutables

La manera más evidente y natural es definir dicha propiedad o capacidad de atributo modificable mediante un var y, a posteriori, mutar su valor.

Este es el caso más natural para un programador (sobre todo si eres recien llegado), dónde se dispone de un punto de guardado o checkpoint de algun valor y a lo largo de la ejecución se va modificando. Sin embargo, en este blog somos alérgicos a lo mutable, nos surje una sensación de urticaria aguda y se nos empieza desorbitar los ojos sólo de pensar en escribir un var.

No sólo por los principales inconvenientes de la programación imperativa (como por ejemplo, que se defina una variable mutable global y que en mitad de un proceso esperes un 4 pero te salta una excepción porque ahi venia un -10. Sino porque, si además damos el salto a sistemas concurrentes y reactivos, ya casi que mejor apagamos y nos vamos.

Inmutabilidad

Efectivamente, necesitamos inmutabilidad.

Porque seamos sincero, ¿Qué es un poco más de código y unos cuantos truquitos de diseños de patrón, para poder conseguir seguridad, corrección y testeabilidad?

Para ello, empecemos hablando de una técnica muy común como es la de sobreescribir esa propiedad en el momento de la creación de la nueva instancia de nuestra clase mediante el uso del override.

Override

Así es, una técnica muy común es crear una nueva instancia y sobrescribir (override) esa propiedad o atributo en el momento de la nueva instanciación.

Sin embargo esto puede generar boilerplate en nuestro código cada vez que tengamos que hacer modificación de una propiedad. Y más aún si necesitamos crear una copia de una instancia para modificar varios valores y que además nos interesa mantener varios atributos concretos cuyo valor no es el de por defecto de su clase o álgebra.

Tendríamos que ir tomando cada uno de estos atributos que queremos mantener, crear una nueva instancia e ir sobrescribiendolos (para matenerlos en la nueva instancia) y por último añadir/sobreescribir (override) el atributo en cuestión objeto de interés que desde un comienzo queriamos modificar.

Caso de uso

Por ello, escribo este artículo para proporcionar un punto de vista de como abordar este problema.

Lo que vamos a hacer, por resumir, es darle a nuestra clase la capacidad de copiarse modificando algún atributo suyo. Esto de dar la capacidad de, es un concepto bastante interesante e importante. Puesto que de la forma en la que vamos a dar esta solución, se puede escalar a cualquier tipo de capacidad o caracteristica que se quiera añadir: capacidad de mostrarse (Show), capacidad de compararse (Eq), capacidad de printearse, de exportarse, de transformarse a otra cosa, etc.

Además, desarrollaremos esta idea de manera bastante modular, intentando evitar a toda costa patrones de tarta o cake patterns.

Observación

Es cierto que existen otras técnicas como el uso de lentes; pero cuando lo que se pretende es algo muy concreto, para un atributo o dos como mucho, no veo la necesidad de usarlas.

Para nuestro relato, partiremos del siguiente caso de uso: nos gustaria desarrollar un sistema que sea capaz de gestionar objetos Config de configuración. Y que además desde este, se extienda varios subtipos (hijos) concretos que puedan aplicarse para entornos (environments) distintos.

Vamos primeramente a definir la copiabilidad como tal, es decir, vamos a proveerle de la capcidad de copiarse (todavia sin tener la posibilidad de modificarse y generar una mutación).

Haciendo composición entre nuestras clases, podemos incorporar y definir el método copy() para cada una de los subtipos.

Con esta implementación, podriamos generar tantas copias de nuestras instancias de ConfigC (por ejemplo), tantas veces como llamadas al método copy() hagamos.

Mutuabilidad inmutable

Pero nuestra principal motivación no era crear copias, sino más bien generar nuevas instancias que mantuvieran los atributos de la instancia original, con atributos modificados de manera inmutable. La idea es básicamente, a partir de nuestro objeto original, crear instancias nuevas pero cambiando el valor de algun atributo en tiempo de creación de esa instancia.

Podemos entonces replantearnos y redefinir nuestro método copy() añadiendole funcionalidad mediante parámetros:

Con esto, hemos conseguido una forma de poder mutar modificando atributos pero sin sobreescribir el valor original, sino haciendo una copia de nuestro objeto mediante copy() y pasándole por parámetros el atributo que queremos modificar.

Conclusión

Acabamos de ver una muy buena entrada al mundo de la mutabilidad desde el punto de vista inmutable. Esta clase de filosofía es bastante típica sobre todo, como comentaba al comienzo del post, en sistemas concurrentes y reactivos. La programación funcional además, favorece el uso de estas técnicas debido al modus operanding de los programadores que usamos Scala o Haskell debido a como el lenguaje está construido o por ser puristas.
 
Si que es cierto que con esto pudiera ser suficiente, nos ha faltado una cosa que podriamos mejorar y que habiamos mencionado como target: que sea modular.
 
La importancia de que sea modular y como la conseguiremos será el tema que trataremos en la segunda parte de este post.

Fechas y Expresiones Regulares

/artista/ unknow

Fechas y expresiones regulares

En el universo data, es muy frecuente tener que estar lidiando con campos de fechas. Ya sea desde el campo más habitual como es un campo de fecha de auditoría a otra clase de campos que den información relevante de un día, mes, año o periodo concreto. O incluso, tener parametrizado nuestro proceso por variables de ejecución cuyos valores sean fechas para por ejemplo, acotar la ejecución por periodos/rangos de fechas.

No es difícil determinar qué función usar para establecer estas fechas, tenemos desde el propio que viene por defecto en Scala (de java), o si estamos usando Spark por ejemplo, la función current_date () : Column, por ejemplo. Lo interesante y “difícil” es establecer un formato correcto o normalizado de estas fechas. Y más interesante, y por tanto más “difícil”, sería conseguir que para cualquier input de fechas en cualquier formato asumible, acabara teniendo un output con nuestro formato normalizado.

Contenido

Regular Expressions o Regex

Para la normalización de fechas, tenemos muchísimos recursos en internet que dan pistas y/o dan soluciones bastante buenas. En este artículo, voy a explicar como normalizar fechas en Scala mediante expresiones regulares (regular expressions o regex) con el fin aceptar cualquier fecha como input (ser flexibles al input) para la ejecución de nuestro proceso (proceso que va a depender de dicha fecha como parámetro de ejecución).

En las referencias al final del articulo dejaré una serie de enlace a sitios donde explican con buen detalle que son las expresiones regulares y herramientas online dónde podréis juguetear con ellas.

Caso de uso

Desarrollaremos esta idea partiendo de un proceso que: Dado una fecha mostrará por pantalla los valores contenidos en un CSV que comprenden por el campo Date desde esa fecha como input hasta la fecha más reciente.

Código Base

Contenido del CSV

El contenido del csv es una descarga del historial de valores que tiene las acciones de Apple. Esta información la hemos podido recoger desde Yahoo Finance. El contenido es el siguiente:

1. Usando precondición o requires

Si nos fijamos en nuestro código de arriba, cualquier fecha que le demos como input con formato distinto al que internamente conocemos como desarrollador, a ojos de usuario nos devolvería un resultado inesperado
sin entender muy bien por qué.

Por ejemplo, si le pasáramos como input 07/10/2021 [mm/dd/yyyy] (formato americano), 07-10-2021 [mm-dd-yyyy] o incluso 10-07-2021 [dd-mm-yyyy], nos mostraría incongruencias. En estos casos, el proceso podría entender que queremos que busque desde el año 7 o 10, respectivamente.

Por ello, una opción podría ser el uso de requires.

Requires

Los requires son requisitos o precondiciones a nivel de usuario de una funcionalidad, de manera que si cierto valor input no cumple la propiedad que requerimos que verifique, automáticamente para todo lo que esté haciendo para saltarte con un error. Añadido a esto, podemos customizar este error dando más
información sobre cual podría ser el problema.

Veamos un ejemplo

Queremos que las fechas que entren como input sean de la forma yyyy-mm-dd. Una precondición podría ser que el separador fuera ‘-’. Otra también podría ser que el primer número sea el año, es decir, que tenga exactamente 4 caracteres. Es decir:

Con esto, somos capaces de dar información al usuario. Por ejemplo, si ejecutamos con 2021/07/10, nos escupe el siguiente error:

Mientras que, si tenemos como input 07-10-2021

2. Usando Regex

Los requires es un buen paso, pero es un poco coñazo estar haciendo constantemente requires. ¿Qué te parece si empezamos a definir un normalizador de fechas? Para empezar, asumiremos que desde origen, nos llega constantemente dos formatos de fecha.
  • dd-mm-yyyy
  • yyyy/mm/dd

La operativa es muy sencilla, y lo que se define para uno, será igual para otro. Lo primero que haremos será definir la expresión regular de por ejemplo yyyy/mm/dd. Este tiene la forma:
([0-9]{4,})([/])([0-9]{2,})([/])([0-9]{2,})

  • [0-9]{4,} – 4 dígitos del 0 al 9
  • ([/]) – Un separador /
  • ([0-9]{2,}) – 2 dígitos del 0 al 9

Para el primer caso sería ([0-9]{2,})([-])([0-9]{2,})([-])([0-9]{4,}). Nuestra función de normalizar fechas quedaría tal que así:

Por supuesto, esto es tan solo un primer acercamiento. De aquí construiremos un constructor inteligente de manera que nos facilite muchísimo más la semántica del código. Pero para conocer los regex, no está nada mal.

Observación

Si nos fijamos, la expresión regular no es más que un string (encapsulado con las dobles comillas) con los patrones comunes de expresiones regulares, y lo importante, acabado en .r De esta forma, un objeto de tipo String ahora sí es de tipo Regex.

Antes de continuar al último paso de generalizar nuestro normalizador para que sea más flexible, construyamos nuestro datatype con constructor inteligente.

2.1 Smart Constructor - StandardDate

Para todos aquellos que no sepan que es un constructor inteligente o Smart Constructor, próximamente haré un articulo sobre ellos y lo útiles e importantes que son de cara al desarrollo.

Pero por ahora, quedaros con la idea de que son una forma óptima, correcta y eficiente de generar datatypes de manera guiada con la intención de evitar errores de ejecución y/o encapsulamientos sin sentido o incorrectos. Un diseño de patrón de construcción más, básicamente.

De manera que ahora nuestra forma de normalizar sea de la siguiente forma:

Con esto tenemos además, la flexibilidad de poder utilizar los parámetros del constructor day, month, y year del datatype StandardDate. Además de poder hacer pattern matching, que a priori, en nuestro caso de uso, quizás no tiene mucha utilidad, pero podría serlo, y sino, al menos a modo ilustrativo. Nuestro smart constructor es la función build(date: String, sep: String = «-«) .

Con esto, podríamos reconfigurar nuestra función principal de showFrom, evitando los requires, y por tanto evitando que rompa el código y asegurando que (casi) siempre pueda funcionar correctamente.

Casi siempre

Porque técnicamente sólo contempla dos patrones de fechas (dos regex). Deberíamos afinar más con controles de errores como Options, Either, etc.

2.2 Smart Constructor [Boosted] (Optional)

Este apartado es totalmente opcional que no interfiere con el tema que estamos tratando, simplemente vamos a afinar nuestro constructor inteligente para que trabaje de una forma correcta.

Para ello implementaremos una sistema de errores de los que a priori se contemple (esto es escalable):

Y añadiremos un control de errores o un error handler (de manera más o menos ad-hoc) mediante el tipo Either que gestione estos errores.

De esta forma el código quedaría de la siguiente forma:

3. Generalizando el uso de Regex

Llegamos a la recta final. ¿Que vamos a hacer? Pues que dado cualquier entrada de fecha podamos dar una fecha estandarizada para nuestro proceso. Contemplaremos, además de los anteriores, los siguientes:
  • yyyy/mm, yyyy-mm, mm/yyyy, mm-yyyy: Con esto que te devuelva el día 1 del mes mm y del año yyyy.
  • yyyy: Con esto que devuelva el día 1 del mes 1 del año yyyy.

Para ello, definiremos una forma genérica de gestionar los regex sin tener que estar constantemente añadiendo casos a nuestra función normalizeDate con pattern matching.

Definamos un nuevo datatype que exprese esta idea de las expresiones regulares para fechas:

De esta forma, para cada expresión que queramos recoger, solo debemos construirla mediante su patrón pattern y su forma de separar las distintas partes cuando haga match. Entonces, todas las formas de dates como input que admitiremos serán de la siguiente forma:

Le hemos dado una vueltita más, y hemos parametrizado el separador, para que podamos construir reconocimientos de patrones en formato dd-mm-yyyy, dd/mm/yyyy, dd#mm#yyyy, dd&&mm&&yyyy etc.

Lo que comentaba, por ejemplo, para el caso FullDateLastYear (que es cuando consideramos el año en última posición), tenemos que definirle el pattern, que sería: «([0-9]{2,})([$sep])([0-9]{2,})([$sep])([0-9]{4,})”

y la función map que configura la forma en la que recoge el patron desde el string cuando haga match:

dónde regex es el Regex que se genera de manera automática cuando se define el pattern.

Entonces, ahora la función normalizeDate de nuestro smart constructor StandardDate, puede ser generalizado y redefinido de la siguiente forma:

Y por tanto, para la ejecución final, definimos nuestra lista implícita de regex.

Código Final

Nuestro código final quedaría de la siguiente forma:

Lo ideal sería tener modularizado todo en una jerarquía de carpetas/objetos y no en un script de scala. Pero eso ya es como uno se organice.

Resultado

Con esto ya podríamos ejecutar nuestra función principal showFrom con un amplio abanico de dates como inputs de manera que el programa se autogestione y sea transparente para nosotros, desde el lenguaje natural.

Conclusión

Desde el punto de vista del desarrollo, esta clase de patrones de diseño que hemos usado para gestionar el uso de patrones con regex es altamente escalable y homogéneo. Lo que facilita el modus operandi de añadir nuevas formas de aceptar los inputs.

Sin embargo, requiere un alto conocimiento de scala a la hora de debuggear. Al final es el problema de siempre, código de lectura fácil en un script línea a línea y poco escalable VS código altamente scalable y generalizado pero requiere conocimiento avanzado.

En mi opinión personal, siempre voy a apoyar un diseño óptimo y escalable frente a los mares de códigos que a veces uno se encuentra. Considero que el desarrollador debe enfrentarse a este nivel de retos y crecer como profesional.