Funtores en Javascript

Introduccion a los functores en JavaScript

Considere la funcion a continuación:

1
2
3
function plus1(value) {
return value + 1
}

Es simplemente una funcion que toma un entero, le adiere 1 y lo retorna. Del mismo modo podriamos tener otra funcion llamada plus2. Vamos a utilizar estas funciones mas adelante.

1
2
3
function plus2(value) {
return value + 2
}

Y ya que estamos, tambien podriamos escribir una funcion generalizada que nos permita usar cualquiera de estas funciones cuando sea necesario:

1
2
3
4
5
function F(value, fn) {
return fn(value)
}
F(1, plus1) ==>> 2

Esta función trabajara correctamente siempre y cuando el valor pasado sea un
numero entero, probemos un array:

1
F([1, 2, 3], plus1) ==>> '1,2,31'

Auch. Tomamos una matriz de enteros, le añadimos un entero y regresamos una cadena!. No solo hizo las cosas mal ya que terminamos con un string habiendo comenzado con un array, sino que nuestro programa tambien a destrozado la estructura de entrada!.

Queremos que F haga “lo correcto” y lo correcto es “mantener la estructura” a trabés de la operación.

Entonces, ¿que es lo que entendemos por “mantener la estructura”?. Nuestra
función debe de “desempaquetar” el array dado y obtener sus elementos para, a continuación, llamar a la función dada con cada elemento del array, luego envolver los valores devueltos en un nuevo array y retornarlo.

Afortunadamente javascript ya tiene esa funcion y se llama map:

1
[1, 2, 3].map(plus1) ==>> [2, 3, 4]

Y map es un functor!

Un functor es una funcion que dado un valor y una función hace lo correcto.

Siendo un poquito mas especifico..

Un functor es una función que dado un valor envuelto (como un array o un objeto por ejemplo) y una función, desenvuelve los valores para llegar a su valor interno, llama a la funcion dada con el valor interno y envuelve los valores devueltos en una nueva estructura para luego retornar esa estructura.

Una cosa a tener en cuenta es que, dependiendo del “tipo” del valor, la
desenvoltura puede dar lugar a un valor o a un conjunto de valores.

Tambien la estructura devuelta no tiene porque ser del mismo tipo que el valor
original. En el caso de map, tanto el valor como el valor devuelto tienen la
misma estructura (un array). La estructura devuelta puede ser de cualquier tipo, siempre y cuando se pueda llegar a los elementos individuales.

Asi que, si habia una funcion que toma un array y retorna un valor de tipo Object con todos los indices del array como llaves, y los valores correspondientes, eso tambien sera un functor.

En el caso de javascript, filter es un functor dado que retorna un array (un valor envuelto en un contexto). Por otro lado, forEach no es un functor porque devuelve undefined. Es decir, forEach no mantiene la estructura.

Los functores provienen de la teoria de categorias de las matematicas. Donde los functores son definidos como “homomorfismos” entre categorias. Vamos a ver que significa eso:

  • homo: igual
  • morfismos: funciónes que mantienen la estructura.
  • category: tipo de dato

Segun la teoria, la funcion F es un functor cuando para 2 funciones comunes
componibles f y g:

1
F(f . g) = F(f) . F(g)

donde el . indica la composición. Los functores deben de preservar la composición.

Asique dada esta ecuación, podemos probar si una funcion dada es de echo un
functor o no lo es.

Functor Array.

Vimos que map es un functor que actua sobre el tipo de dato array. Vamos a
demostrar que la función de javascript Array.map es un functor.

1
2
3
function compose(f, g) {
return function(x) {return f(g(x))}
}

La composición de funciones se trata de llamar a un conjunto de funciones, llamando a la funcion siguiente con los resultados de la funcion anterior.

Tenga en cuenta que nuestra función compose funciona de derecha a izquierda, osea g se llama primero y luego f.

1
2
3
[1, 2, 3].map(compose(plus1, plus2)) ==>> [ 4, 5, 6 ]
[1, 2, 3].map(plus2).map(plus1) ==>> [ 4, 5, 6 ]

Por lo visto sip, map es de echo un functor.

Vamos a probar algunos functores. Podemos escribir functores para valores de
cualquier tipo, siempre y cuando se pueda desenvolver el valor y retornar la
estructura.

String functor.

Entonces… ¿podemos escribir un functor de tipo string?, ¿se puede desenvolver un string?. En realidad si se puede, si se piensa en una cadena como un array de caracteres.

Esto se trata realmente acerca de como se mire el valor. Tambien sabemos que los caracteres tienen codigos que son numeros enteros, entonces si ejecutamos plus1 en cada charcode envolviendolos de nuevo en un
string y retornandolas…

1
2
3
4
5
6
7
8
9
function stringFunctor(value, fn) {
var chars = value.split("");
return chars.map ( function(char) {
return String.fromCharCode( fn( char.charCodeAt(0) ) );
})
.join(""); //Nuevamente volvemos a "empaquetar el valor"
}
stringFunctor("ABCD", plus1) ==>> "BCDE"

Usted podra comenzar a ver como son de impresionantes los functores,
en realidad se puede escribir un parser usando el functor string como base.

Functor funcion.

Como sabemos, en javascript las funciones son ciudadanos de primera clase. Esto significa que podemos tratar a las funciones como a cualquier otro valor.
Entonces, ¿Podemos escribir un functor para un valor de tipo function?,
deberiamos de ser capaces de hacerlo, pero ¿como desenvolvemos una funcion?.

Se puede desenvolver una funcion llamandola y consiguiendo su valor de retorno, pero enseguida nos encontraremos con un problema, y es que para llamar a una funcion necesitamos sus argumentos. Recordemos que un functor solo tiene la funcion de entrada como el valor.

Podemos solventar esto haciendo que el functor devuelva una nueva funcion. Esta funcion sera llamada con los argumentos y que a su vez, llamara a la funcion de valor con los argumentos, para luego llamar a la función del functor original con el valor devuelto.

1
2
3
4
5
6
7
8
9
10
11
function functionFunctor(value, fn) {
return function(initial) {
return function() {
return fn(value(initial))
}
}
}
var init = functionFunctor(function(x) {return x * x}, plus1)
var final = init(2)
final() ==> 5

Nuestro functor funcion realmente no hace mucho, por decir poco. Pero hay un par de cosas para tomar nota aqui. Nada sucede hasta que se llame a final. Cada cosa esta en un estado de animacion suspendida hasta que final sea llamado. El functor funcion (tambien llamado aplicativo) constituye la base para la materia funcional mas impresionante como el mantenimiento del estado, llamadas de continuación e incuso promesas. Usted puede escribir sus propios functores funcion para hacer todas estas cosas.

Functor Maybe

1
2
3
function mayBe(value, fn) {
return value === null || value === undefined ? value : fn(value)
}

Si, esto es un functor valido.

1
2
3
4
MayBe(undefined, compose(plus1, plus2)) ==>> undefined
mayBe(mayBe(undefined, plus2), plus1) ==>> undefined
mayBe(1, compose(plus1, plus2)) ==>> 4
mayBe(mayBe(1, plus2), plus1) ==>> 4

Asique Maybe pasa nuestra prueba de functor. No hay necesidad de envolver o
desenvolver aqui. Simplemente retorna Nothing para nothing. Maybe se usa como una funcion de corto circuito, que sirve como un sustituto de codigo
para cosas como:

1
2
3
4
5
if (result === null) {
return null
} else {
doSomething(result)
}

Funcion identidad

1
2
3
function id(x) {
return x
}

A la función anterior se la conoce como la funcion identidad. Es simplemente una funcion que retorna el valor que se le pasa commo argumento (bastante simple no?).
Se llama asi ya que es la identidad en la composicion de funciones en las matematicas.

Hemos aprendido anteriormente que los functores deben preservar la composicion. Sin embargo algo que no hemos mencionado aqui es que los functores tambien deben de preservar la identidad, es decir:

1
F(value, id) = value

Vamos a probar esto para map.

1
[1, 2, 3].map(id) ==>> [ 1, 2, 3 ]

Signatura de tipo.

El tipo de firma de una función es su argumento o argumentos de ser mas de uno y el valor de retorno.

Por lo que el tipo de firma de la funcion plus1 es:

1
f: int -> int

El tipo de signatura de la función map depende del tipo de signatura de la
función pasada como argumento.Entonces si map es llamado con plus1 entonces este es su tipo de signatura:

1
map: [int] -> [int]

Sin embargo el tipo de firma de la funcion dada no tiene porque ser como la
anterior. Tranquilamente podriamos tener una funcion como:

1
f: int -> string

En el que el tipo de signatura de map seria:

1
map: [int] -> [string]

La unica reestricción es que el cambio de tipo no afecte a la componibilidad del functor. Asique en general una firma de tipo functor puede.

1
F: a -> b

En otras palabras, map puede tomar un array de enteros y retornar un array de cadenas y seguir siendo un functor valido.

Las monadas son un caso especial de los functores cuya firma es:

1
M: A -> A

Pero veremos mas acerca de las monadas en proximos articulos, por hoy creo que tenemos suficiente para entretenernos un buen rato.

Comentarios