Descripción general
En la programación funcional, un functor es esencialmente una construcción de levantar funciones unarias ordinarias (es decir, aquellas con un argumento) para funciones entre variables de nuevos tipos. Es mucho más fácil escribir y mantener funciones simples entre objetos simples y usar functores para levantarlos, luego escribir funciones manualmente entre objetos contenedores complicados. Otra ventaja es escribir funciones simples solo una vez y luego reutilizarlas a través de diferentes functores.
Los ejemplos de functores incluyen matrices, functores "tal vez" y "cualquiera", futuros (ver, por ejemplo, https://github.com/Avaq/Fluture ) y muchos otros.
Ilustración
Considere la función que construye el nombre completo de la persona a partir del nombre y apellido. Podríamos definirlo fullName(firstName, lastName)
como una función de dos argumentos, que, sin embargo, no sería adecuado para los functors que solo se ocupan de las funciones de un argumento. Para remediar, recopilamos todos los argumentos en un solo objeto name
, que ahora se convierte en el argumento único de la función:
// In JavaScript notation
fullName = name => name.firstName + ' ' + name.lastName
Ahora, ¿qué pasa si tenemos muchas personas en una matriz? En lugar de repasar manualmente la lista, simplemente podemos reutilizar nuestra función a fullName
través del map
método proporcionado para matrices con una sola línea de código corta:
fullNameList = nameList => nameList.map(fullName)
y úsalo como
nameList = [
{firstName: 'Steve', lastName: 'Jobs'},
{firstName: 'Bill', lastName: 'Gates'}
]
fullNames = fullNameList(nameList)
// => ['Steve Jobs', 'Bill Gates']
Eso funcionará, siempre que cada entrada en nuestro nameList
sea un objeto que proporcione propiedades firstName
y lastName
propiedades. Pero, ¿qué pasa si algunos objetos no lo hacen (o incluso no son objetos)? Para evitar los errores y hacer que el código sea más seguro, podemos envolver nuestros objetos en el Maybe
tipo (por ejemplo, https://sanctuary.js.org/#maybe-type ):
// function to test name for validity
isValidName = name =>
(typeof name === 'object')
&& (typeof name.firstName === 'string')
&& (typeof name.lastName === 'string')
// wrap into the Maybe type
maybeName = name =>
isValidName(name) ? Just(name) : Nothing()
donde Just(name)
es un contenedor que lleva solo nombres válidos y Nothing()
es el valor especial utilizado para todo lo demás. Ahora, en lugar de interrumpir (u olvidar) para verificar la validez de nuestros argumentos, simplemente podemos reutilizar (levantar) nuestra fullName
función original con otra sola línea de código, basado nuevamente en el map
método, esta vez provisto para el tipo Quizás:
// Maybe Object -> Maybe String
maybeFullName = maybeName => maybeName.map(fullName)
y úsalo como
justSteve = maybeName(
{firstName: 'Steve', lastName: 'Jobs'}
) // => Just({firstName: 'Steve', lastName: 'Jobs'})
notSteve = maybeName(
{lastName: 'SomeJobs'}
) // => Nothing()
steveFN = maybeFullName(justSteve)
// => Just('Steve Jobs')
notSteveFN = maybeFullName(notSteve)
// => Nothing()
Teoría de la categoría
Un Functor en la teoría de categorías es un mapa entre dos categorías que respeta la composición de sus morfismos. En un lenguaje de computadora , la categoría principal de interés es aquella cuyos objetos son tipos (ciertos conjuntos de valores) y cuyos morfismos son funciones f:a->b
de un tipo a
a otro b
.
Por ejemplo, tome a
como el String
tipo, b
el tipo Número y f
es la función que asigna una cadena a su longitud:
// f :: String -> Number
f = str => str.length
Aquí a = String
representa el conjunto de todas las cadenas y b = Number
el conjunto de todos los números. En ese sentido, tanto a
y b
representar objetos en la categoría de conjunto (que está estrechamente relacionado con la categoría de tipos, con la diferencia de que aquí no esencial). En la categoría de conjunto, los morfismos entre dos conjuntos son, precisamente, todas las funciones desde el primer conjunto hasta el segundo. Entonces, nuestra función de longitud f
aquí es un morfismo del conjunto de cadenas en el conjunto de números.
Como solo consideramos la categoría de conjunto, los Functores relevantes de ella en sí mismos son mapas que envían objetos a objetos y morfismos a morfismos, que satisfacen ciertas leyes algebraicas.
Ejemplo: Array
Array
puede significar muchas cosas, pero solo una es Functor: la construcción de tipos, que asigna un tipo a
al tipo [a]
de todas las matrices de tipos a
. Por ejemplo, el Array
functor asigna el tipo String
al tipo [String]
(el conjunto de todas las matrices de cadenas de longitud arbitraria) y establece el tipo Number
en el tipo correspondiente [Number]
(el conjunto de todas las matrices de números).
Es importante no confundir el mapa Functor
Array :: a => [a]
con un morfismo a -> [a]
. El functor simplemente asigna (asocia) el tipo a
en el tipo [a]
como una cosa a otra. Que cada tipo es en realidad un conjunto de elementos, no tiene relevancia aquí. Por el contrario, un morfismo es una función real entre esos conjuntos. Por ejemplo, hay un morfismo natural (función)
pure :: a -> [a]
pure = x => [x]
que envía un valor a la matriz de 1 elemento con ese valor como entrada única. ¡Esa función no es parte del Array
Functor! Desde el punto de vista de este functor, pure
es solo una función como cualquier otra, nada especial.
Por otro lado, el Array
Functor tiene su segunda parte: la parte del morfismo. Que mapea un morfismo f :: a -> b
en un morfismo [f] :: [a] -> [b]
:
// a -> [a]
Array.map(f) = arr => arr.map(f)
Aquí arr
hay cualquier conjunto de longitud arbitraria con valores de tipo a
, y arr.map(f)
es el conjunto de la misma longitud con valores de tipo b
, cuyas entradas son resultados de aplicar f
a las entradas de arr
. Para convertirlo en un functor, deben cumplir las leyes matemáticas de mapeo de identidad a identidad y de composiciones a composiciones, que son fáciles de verificar en este Array
ejemplo.