Programación Funcional en JavaScript: La aridad y las tuplas

14787298814_2ed9b48aa0_k.jpg

Volvemos a la serie de posts dedicados a la programación funcional en JavaScript. Nos habíamos quedado explicando la importancia del uso de los métodos encadenados para conseguir código más expresivo.

En el posts de hoy hablaremos de las desventajas que nos acarrea el uso de métodos encadenados. Aprenderemos una nueva forma de modularizar nuestro código y de sacar mucho más partido a la reutilización de código por medio del concepto de tuberías que se puede usar en programación funcional.

Explicaremos qué son las tuberías y las características que necesitamos que cumplan las funciones para poder conseguir esa sensación de código que fluye por medio de una tubería conectada. Explicaremos qué es la aridad y cómo las tuplas o la currificación pueden ayudarnos a manejar la complejidad de aridades elevadas.

Como siempre, me tenéis a vuestra disposición en los comentarios por si pudiera resolver alguna de vuestras dudas. Empecemos:

El problema de los métodos encadenados

Los métodos encadenados nos ayudan mucho a escribir mejor código. Lo pudimos ver en el capítulo anterior. Eso no cambia. Sin embargo, encontramos un gran problema a los métodos encadenados.

Si nos acordamos por ejemplo del método ‘filter’ de ES5, el método devuelve un nuevo objeto array que contiene de nuevo los métodos. De ahí la magia de poder concatenar. Desafortunadamente, el patrón es un arma de doble filo ya que los métodos se encuentran tan acoplados al tipo de objeto que los confina que nos da muy poca flexibilidad de reutilización.

Con métodos concatenados estamos obligados a usar solo el conjunto de operaciones que proporciona el propio objeto. Sería ideal que pudiéramos romper esta cadena y que dentro del montaje pudiéramos organizar funciones a nuestro antojo. Los métodos encadenados no son la solución, así que tendremos que buscarnos otra alternativa.

Las tuberías

Y la alternativa viene en forma de tubería. Si con los métodos encadenados perdíamos expresividad y ganabamos demasiado acoplamiento, con la organización de funciones en una tubería podríamos tener todo lo contrario.

La clave de organizar funciones en tuberías es la de poder contar con un arsenal de funciones desacopladas que se puedan ejecutar secuencialmente. Como en una tubería, nuestras funciones pasarían estados de una función a otra. Los parámetros de salida de una función supondrán los parámetros de entrada de la siguiente.

De esta forma, yo puedo crear piezas de código muy independientes, que cumplen una función concreta y que por composición de sus entradas y sus salidas puedan crear un programa mayor.

Si usamos la notación usada en Haskell, podremos explicar mejor esto. Podemos definir funciones de esta manera:

<nombre_funcion> :: <entradas> -> <salidas>

Al final es muy parecido a definir lambdas, solo que en este caso, nos interesa más saber que tipos de datos entran y salen de una función. Es la especificación de una caja negra.

Visto esto, veamos un ejemplo. Yo puedo componer por medio de tuberías las siguientes funciones:

f :: A -> B
g :: B -> C

donde f y g son el nombre de una función y A, B y C son tipos diferentes. En este caso como la salida de f coincide con el tipo de B. Yo podría ejecutarlas como si fuese una tubería:

g(f(A)); // donde se devuelve C

Esto que a simple vista parece obvio, puede ayudarnos mucho. No será fácil conseguir esto en entornos más complejos ya que habrá funciones que devuelvan objetos que no puedan ser reutilizados.

Sin embargo, es algo que tenemos que tener en cuenta a la hora de diseñar funciones. Es importante ver que necesitamos para hacer que las funciones sean compatibles entre si. Para concienciarnos de ello en el proceso de diseño, es importante que tengamos en cuenta dos factores: la compatibilidad de tipos de las entradas y salidas y la aridad de las funciones.

Compatibilidad de tipos

Como venimos diciendo, la compatibilidad de tipos hace referencia a que los tipos de salida de función se relacionan con los tipos de los parámetros de entrada de otra. No es un problema del que tengamos que preocuparnos mucho en JavaScript pues al final al encontrarnos con un lenguaje débilmente tipado, podemos tener mucha más flexibilidad.

Al final esto es lo que se suele denominar ‘duck-types’, no me interesa en las tuberías de JavaScript tanto que los tipos sean idénticos como que el objeto que devuelve una función, se comporte como el el tipo del parámetro de entrada que espera mi siguiente función. Es decir, si anda como un pato y grazna como un pato, es un pato. Si un objeto se comporta un tipo determinado y tiene los atributos que yo espero en este tipo, para mi es de ese tipo. Es lo bueno y lo malo de JavaScript.

Si quisiéramos hacer nuestras funciones estrictamente compatibles, una buena idea sería usar TypeScript. Os dejo la charla de Micael Gallego de nuevo por aquí por si os interesa esta alternativa.

Por tanto, cuando diseñemos funciones, tenemos que tener muy claros los tipos con los que juegan nuestras funciones. Imaginemos que queremos hacer un sistema que dado un número de identidad, le quitemos los espacios y los guiones para trabajar mejor con ello. Diseñaremos dos funciones como estas:

trim :: String -> String
normalize :: String -> String

Hemos creado dos funciones compatibles para su composición ya que la entrada de una coincide con el tipo de la otra. Además son conmutativas porque si yo ejecuto trim y luego normalize, obtengo el mismo resultado que si ejecuto primero ‘normalize’ y luego ‘trim’. Veamos la implementación.

// trim :: String -> String
const trim = (str) => str.replace(/^\s*|$/g, '');

// normalize :: String -> String
const normalize = (str) => str.replace(/\-/g, '');

normalize(trim('444-444-444')); // 444444444
trim(normalize('444-444-444')); // 444444444

Vale, el ejemplo es muy tonto, pero es importante que tengamos conciencia de ello en el futuro. Tendremos más problemas en JavaScript con el número de parámetros que puede permitir una función.

La aridad y las tuplas

Conocemos por aridad al número de parámetros de entrada que acepta una función. Es común indicarlo también como longitud de una función.

La aridad de una función puede hacer que una función gane en complejidad. Que una función acepta varios parámetros hace que todos los argumentos hayan tenido que ser calculados primero, esto les hace perder versatilidad.

Además, puede darse el caso que la complejidad interna de una función sea mayor (que haya gran aridad no comporta siempre mayor complejidad, simplemente puede ser un síntoma de ello).

Contar con aridades elevadas además, hace que tengamos funciones menos flexibles. Las funciones con un parámetro (o unarias) suelen ser más flexibles porque, por lo general, solo tienen una única responsabilidad, realizar algún tipo de operación o calculo sobre el parámetro de entrada y devolver el resultado.

Desafortunadamente, en la vida real es complicado encontrarse con solo funciones unarias. Sin embargo, podemos hacer que una función devuelve una tupla, de esta manera la siguiente función sería unaria y ya contaría con todo el estado que necesita.

Una tupla es un listado finito y ordenado de valores que se representan de la siguiente forma (a, b, c). Las tuplas son un paquete de valores inmutables, que tienen una relación y que se pueden usar para devolver varios valores a otras funciones.

Podemos simular este comportamiento con un objeto JSON o con objetos array, si embargo como vemos a continuación con las tuplas tenemos más ventajas:

  • Son inmutables.
  • Evitan la creación de nuevos tipos. Muchas veces crear clases para un único uso específico puede ser bastante poco usable para el desarrollador.
  • Evitan crear arrays heterogéneos: trabajar con estos arrays heterogéneos supone mucho desarrollo de comprobaciones de tipos. Los arrays es mejor solo usarlos cuando se trabaja con colecciones de objetos con el mismo tipo.

El problema que tenemos, es que aunque las tuplas es un elemento en la mayoría de lenguajes de programación funcional como Scala, en JavaScript no existe nada que dé soporte a las tuplas. La mejor opción para esto es que nosotros creemos una pequeña librería que de soporte a las tuplas. La opción que Luis Atencio propone en su libro es esta:

const Tuple = function () {
    const typeInfo = Array.prototype.slice.call(arguments, 0);
    const _T = function () {
        const values = Array.prototype.slice.call(arguments, 0);
        
        if (values.some((val) => val === null || val === undefined) {
             throw new ReferenceError('Tuples may not have any null values');
        }   

        if (values.length !== typeInfo.length) {
            throw new TypeErro('Tuple arity does not match its prototype');
        }

        values.map(function (val, index) {
            this['_' + (index + 1)] = checkType(typeInfo[index])(val);
        }, this);

        Object.freeze(this); 
    };

    _T.prototype.values = function () {
        return Object.keys(this).map(function (k) {
            return this[k];
        }
    }
    return _T;
}

Uf, si, mucha chicha que cortar. Si nos damos cuenta es una función Tuple que va a devolver una clase. La función sirve para que podamos indicar el tamaño y los tipos de los elementos de la tupla. Por tanto podremos hacer esto, por ejemplo:

const Status = Tuple(Number, String);

‘Status’ es una clase. Si os dais cuenta Tuple, está devolviendo _T. Por medio de un clousure se nos devuelve una clase preconfigurada con lo que necesitamos. Mágico. Ahora podemos definir una nueva tupla de esta forma:

const status = new Status(401, 'No Authorize');

Cuando instanciamos ‘status’ hace una comprobación de que no se están pasando valores nulos y que el tamaño de la tupla creada corresponde con el tamaño de la tupla predefinida. También comprueba que el tipo que se ha pasado es el esperado.

Por último tiene un método para que podamos obtener los valores en formato array. Es muy útil porque si usamos de nuevo la desestructuración, podemos obtener los valores de la tupa de la esta manera:

const [code, message] = status.values();

Bueno… no son lo más cómodo del mundo las tuplas en JavaScript, pero tenemos formas de simularlo. ¿Y si no tuviéramos que simularlas? ¿Y si JS ya las trajera de serie? Pues otra vez TypeScript nos ayuda en esto.  Yo en TS podría hacer esto:

let status: [number, string]; 
status = [401, 'Not authorize']; // OK 
status = ['Not authorize', 401]; // Error

Mucho mejor :). Vuelvo a repetir, tened en cuenta a TypeScript, ha venido para quedarse.

Las tuplas son un buen mecanismo para evitar la aridad de funciones. Sin embargo existe otro método denominado currificación, que no solo nos ayudará con la aridad, sino que es un buen mecanismo para mejorar la modularidad y la reusabilidad, pero… creo que lo dejaremos aquí por hoy 🙂

Conclusión

Entender las tuberías y su formas de usarlas, nos ayudará a desarrollar código más legible. Seguiremos hablando de técnicas necesarias para abarcar todos los conceptos que se encuentran presentes en las tuberías.

Por ahora, creo que es un buen paso saber que diseñar funciones de forma que tengan una aridad reducida y que conocer los tipos de los parámetros de entrada y de salida de nuestras funciones, nos ayudarán para realizar código mucho más mantenible, modularizable y reutilizable.

Nos leemos 🙂

PD: Os dejo el mapa conceptual de esta entrada:

las-tuberias-en-javascript

Anteriores posts de Programación Funcional en JavaScript:

Introducción

  1. La Programación Funcional en JavaScript
  2. Programación Funcional en JavaScript: Los Objetos
  3. Programación Funcional en JavaScript: Las funciones

El control de flujo funcional

  1. Programación Funcional en JavaScript: Los métodos funcionales
  2. Programación Funcional en JavaScript: La recursividad

La modularidad funcional

  1. Programación Funcional en JavaScript: La aridad y las tuplas
  2. Programación Funcional en JavaScript: La currificación

Imagen de portada | flickr

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s