Functores paso a paso en JavaScript

Completa introducción a los functores

una explicación paso a paso sobre como funcionan estos poderosos contenedores.

Supongamos que tenemos un valor comun y corriente:

1
var valor = 2;

2

Y deseamos utilizarlo en una función que le sume 1 a ese valor.

1
2
3
var valor = 2;
function addOne(value) { return value +1; }
addOne(valor);

addOne(value) => 3

Como era de esperarse, funciona sin problemas. ¿que pasaria si en vez de pasarle un numero entero algun programador malvado le pasara como argumento un numero entre ' '?.

1
2
3
var valor = 2;
function addOne(value) { return value +1; }
addOne('2');

addOne('2') => '21'

mmm seguramente no era eso lo que deseabamos. ¿Porque paso esto?, bueno de echo como javascript no es un lenguaje fuertemente tipado, una variable puede ser de cualquier tipo, un numero, un string, un objeto, un array, etc…., al igual que el valor que le pasemos como parametro.

Parece que necesitamos algun tipo de validación, para que no nos pasen cualquier cosa como parametro, vamos a implementar algo cutre que nos salve un poco el cuello.

1
2
3
4
5
6
7
8
9
10
11
var valor = 2;
function addOne(value) {
return typeof(value) === "number"
? value + 1
: parseInt(value) +1;
}
var result = addOne(valor); // ==> 3
result = addOne('2'); // ==> 3
result = addOne([5]); // ==> 6

addOne('2') ==> 3

Funciona, tanto para strings como para arrays (puesto que un string no es mas que un array de caracteres, es algo logico). Aunque esta implementación funcione, aun deja mucho que desear puesto que en realidad tiene un par de fallas por depender de variables globales. ¿que pasaria si alguien cambiara el tipo de la variable global valor?, que tal si desearamos concatenar una serie de llamadas a la función? ej:

1
2
3
4
var result = addOne(valor); // ==> 3
result += addOne('2'); // ==> 6
result += addOne('a'); // ==> NaN
result += addOne(2); // ==> NaN

Lo ideal seria que obviara el tipo de dato que desconoce 'a' (en este caso) y continuara con el calculo, pero eso no es posible debido a que la funcion addOne no tiene una forma de almacenar su valor e ir incrementando el resultado, tambien estaria bueno que esa variable global valor tuviera cierta proteccion durante nuestro calculo, asi evitariamos que alguna función pueda modificar su valor mientras estamos realizando nuestros calculos en base a ella.

Tambien hay otro problema y es que el tipo de dato number no sabe como trabajar con valores que no sean de su mismo tipo, al igual que los strings , las listas, los Object, etc…

¿Porque entonces no nos inventamos un tipo de dato que no tenga estos problemas?, pues bien, veamos que nos hace falta:

  1. Que sea agnostico al tipo de dato, osea que pueda trabajar con string, numeros, objetos, etc…
  2. Que nuestros datos esten totalmente protegidos.
  3. Una manera de aplicarle funciones a nuestro tipo de dato.
  4. Una manera de “encadenar funciones”, sobre nuestro tipo de dato.

Si logramos todo eso tendriamos….

Nuestro primer functor

Vamos a crear nuestro primer functor, por ahora no le daremos muchas caracteristicas para no complicar el codigo, pero poco a poco lo iremos mejorando…

1
2
3
4
5
6
7
8
9
10
11
12
13
var Functor = function(value) {
this.__value = value;
}
Functor.of = function(value) {
return new Functor(value);
}
Functor.prototype.map = function(fn) {
return Functor.of( fn (this.__value) );
}
Functor.of(2).map(addOne).map(addOne).map(console.log);

primer_functor

¡Impresionante!, ¿que es eso de la caja fuerte?. Cuando creamos un functor, en realidad lo que estamos haciendo es encerrar continuamente el valor dentro de un contexto. Miremos la linea 6 y veremos que cada vez que llamamos al metodo of pasandole el valor como argumento, lo que hacemos es “encerrarlo” dentro de una nueva instancia del functor.

1
2
3
Functor.of = function(value) {
return new Functor(value);
}

Esto nos permite mantener nuestros valores protegidos de amenazas externas como puede ser una llamada asincrona que se ejecuto fuera de tiempo, cambios por medio de funciones a nuestro valor, errores de tipos de datos, etc…

funcion_rota

Esto falla, porque la función addOneno sabe como desenvolver el functor para extraer el valor.

Los functores son muy utiles cuando tenemos un valor y necesitamos aplicarle un conjunto de transformaciónes para luego hacer algo con él.

Como hemos visto, ya por el solo echo de ser un functor, podemos encadenarle funciones a nuestro valor, sin necesidad de implementar codigo especifico para ese proposito. Tambien nos ha brindado una manera de apicarle funciónes a nuestros valores, de un modo totalmente seguro, a trabes del metodo map. Y ni hablar de la proteción de nuestros valores!!.

Ahora ¿realmente es agnostico a los tipos de datos?.
¿Puedo usarlo tanto con un numero, como con un string o incluo con un objeto si quisiera?.

Pues veamos un ejemplo y de paso introduciremos levemente ramda una utilisima libreria que nos brinda unas cuantas funciónes para facilitarnos la vida con la programación funcional en JavaScript.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var _ = require("ramda");
var Functor = function(value) {
this.__value = value;
}
Functor.of = function(value) {
return new Functor(value);
}
Functor.prototype.map = function(fn) {
return Functor.of(fn(this.__value) );
}
Functor.of("Los functores son geniales!!!")
.map(_.replace("geniales","copados"))
.map(_.toUpper)
.map(_.split(" "))
.map(_.last)
.map(console.log);

Genial, probemos ahora con una lista de objetos a ver que pasa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var _ = require("ramda");
var Personas = [
{nombre: "carlos", edad:20, sexo:"M"},
{nombre: "gisela", edad:26, sexo:"F"},
{nombre: "dario", edad:30, sexo:"M"},
{nombre: "cecilia", edad:35, sexo:"F"},
{nombre: "ambar", edad:7, sexo:"F"},
{nombre: "oscar", edad:57, sexo:"M"},
{nombre: "facundo", edad:37, sexo:"M"}
];
Functor.of(Personas)
.map(_.filter((persona) => persona.edad <=8 )) //Filtra todo el array de objetos por edad.
.map(_.head) //Obtiene la primera coincidencia
.map(_.prop("nombre")) //Obtiene el nombre de la persona
.map(_.concat("Salistes ganando ")) //Le concatena una frase.
.map(_.split(" ")) //transforma el string en un array
.map(_.reverse) //invierte los valores del array
.map(_.join(" ")) //los vuelve a concatenar
.map(console.log) //y ya aburrido de jugar,
//lo muestra por la consola

Como vemos son bastante poderosos y nos permiten agrupar una serie de transformaciones sobre los valores de una forma bastante sencilla. Tambien te habras dado cuenta de que realmente es agnostico de los datos, ya que en el ultimo ejemplo, comenzamos con una lista de objetos y terminamos con undefined (un console.log) retorna eso.

Pues bien, ya para ir cerrando este primer articulo vamos a implementar una manera de controlar los errores.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var Maybe = function(x) {
this.__value = x;
};
Maybe.of = function(x) {
return new Maybe(x);
};
Maybe.prototype.isNothing = function() {
return (this.__value === null || this.__value === undefined);
};
Maybe.prototype.map = function(f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
};
//Cuenta la cantidad de iniciales en el nombre
Maybe.of("Mariano Martinez")
.map(_.match(/m/ig))
.map(_.length)
.map(console.log);
//Intenta extraer una propiedad no existente en el objeto
//y al no encontrarla simplemente corta el proceso sin generar errores.
Maybe.of({name: 'carlos'})
.map(_.prop('edad'))
.map(_.add(10))
.map(console.log);
//El mismo codigo que arriba pero esta vez
//si encuentra la propiedad buscada, por lo tanto la muestra en pantalla.
Maybe.of({name: 'carlos', edad: 20})
.map(_.prop('edad'))
.map(_.add(10))
.map(console.log);

Impresionante no?. Todo esto es posible porque existe una fuerte teoria matematica detras de los functores (de echo, todo lo que se usa en la programacion funcional tiene una fuerte teoria matematica que lo respalda). Vamos a analizarla:

Ley de la identidad.

map(id) == id - es decir, si intentamos mapear id sobre los elementos de algún contenedor, nos debe de dar lo mismo a que si no hubiésemos hecho nada (sin efectos secundarios).

Ley de composición

compose(map(f), map(g)) === map(compose(f, g)); - dice que, mapear (f) y mapear(g) y luego componerlas juntas, es lo mismo que mapear la composicion de las 2 funciones.

No te preocupes si todavia no te queda demasiado claro el tema de las leyes, poco a poco a los largo de los articulos que vienen lo vamos a ir tocando hasta que te lo sepas de memoria casi sin darte cuenta.

Resumiendo:

Un functor entonces, no es mas que un tipo de dato, que permite aplicar funciones a un valor envuelto.

Palabras finales

Existen muchas cosas que se pueden hacer con functores, recien hemos rozado la punta del iceberg. Existen tambien otro tipo de functores llamados aplicativos que francamente no tienen mucho uso pero que a su tiempo hablaremos sobre ellos ya que son la base para entender las monadas.

¿Que sigue?, en proximos articulos hablaremos acerca de la composicion funcional, una tecnica que te enamorara a primera vista. Pero mientras… ¡ha practicar con los functores!! :)

Comentarios