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 fullNametravés del mapmé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 nameListsea un objeto que proporcione propiedades firstNamey lastNamepropiedades. 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 Maybetipo (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 fullNamefunción original con otra sola línea de código, basado nuevamente en el mapmé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->bde un tipo aa otro b.
Por ejemplo, tome acomo el Stringtipo, bel tipo Número y fes la función que asigna una cadena a su longitud:
// f :: String -> Number
f = str => str.length
Aquí a = Stringrepresenta el conjunto de todas las cadenas y b = Numberel conjunto de todos los números. En ese sentido, tanto ay brepresentar 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 faquí 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
Arraypuede significar muchas cosas, pero solo una es Functor: la construcción de tipos, que asigna un tipo aal tipo [a]de todas las matrices de tipos a. Por ejemplo, el Arrayfunctor asigna el tipo String al tipo [String](el conjunto de todas las matrices de cadenas de longitud arbitraria) y establece el tipo Numberen 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 aen 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 ArrayFunctor! Desde el punto de vista de este functor, purees solo una función como cualquier otra, nada especial.
Por otro lado, el ArrayFunctor tiene su segunda parte: la parte del morfismo. Que mapea un morfismo f :: a -> ben un morfismo [f] :: [a] -> [b]:
// a -> [a]
Array.map(f) = arr => arr.map(f)
Aquí arrhay 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 fa 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 Arrayejemplo.