Visão geral aproximada
Na programação funcional, um functor é essencialmente uma construção de elevação de funções unárias comuns (isto é, aquelas com um argumento) para funções entre variáveis de novos tipos. É muito mais fácil escrever e manter funções simples entre objetos simples e usar functors para levantá-los e depois escrever funções manualmente entre objetos contêineres complicados. Outra vantagem é escrever funções simples apenas uma vez e depois reutilizá-las através de diferentes functores.
Exemplos de functores incluem matrizes, "talvez" e "qualquer" functores, futuros (consulte, por exemplo, https://github.com/Avaq/Fluture ) e muitos outros.
Ilustração
Considere a função que constrói o nome completo da pessoa a partir do nome e sobrenome. Poderíamos defini-lo como fullName(firstName, lastName)
função de dois argumentos, que, no entanto, não seriam adequados para functores que lidam apenas com funções de um argumento. Para remediar, coletamos todos os argumentos em um único objeto name
, que agora se torna o único argumento da função:
// In JavaScript notation
fullName = name => name.firstName + ' ' + name.lastName
Agora, e se tivermos muitas pessoas em uma matriz? Em vez de revisar manualmente a lista, podemos simplesmente reutilizar nossa função fullName
através do map
método fornecido para matrizes com uma única linha de código curta:
fullNameList = nameList => nameList.map(fullName)
e use-o como
nameList = [
{firstName: 'Steve', lastName: 'Jobs'},
{firstName: 'Bill', lastName: 'Gates'}
]
fullNames = fullNameList(nameList)
// => ['Steve Jobs', 'Bill Gates']
Isso funcionará, sempre que cada entrada no nosso nameList
for um objeto que fornece propriedades firstName
e tanto lastName
. Mas e se alguns objetos não o fizerem (ou nem sequer são objetos)? Para evitar os erros e tornar o código mais seguro, podemos agrupar nossos objetos no Maybe
tipo (por exemplo, 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()
onde Just(name)
é um contêiner que contém apenas nomes válidos e Nothing()
é o valor especial usado para todo o resto. Agora, em vez de interromper (ou esquecer) para verificar a validade de nossos argumentos, podemos simplesmente reutilizar (elevar) nossa fullName
função original com outra linha de código única, baseada novamente no map
método, desta vez fornecido para o tipo Maybe:
// Maybe Object -> Maybe String
maybeFullName = maybeName => maybeName.map(fullName)
e use-o 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()
Teoria da categoria
Um Functor na Teoria da Categoria é um mapa entre duas categorias, respeitando a composição de seus morfismos. Em uma linguagem de computador , a principal categoria de interesse é aquela cujos objetos são tipos (determinados conjuntos de valores) e cujos morfismos são funções f:a->b
de um tipo a
para outro tipo b
.
Por exemplo, considere a
ser o String
tipo, b
o tipo Number e f
é a função que mapeia uma sequência em seu comprimento:
// f :: String -> Number
f = str => str.length
Aqui a = String
representa o conjunto de todas as strings e b = Number
o conjunto de todos os números. Nesse sentido, ambos a
e b
representam objetos na Categoria de conjunto (que está intimamente relacionada à categoria de tipos, com a diferença aqui não essencial). Na categoria de conjuntos, os morfismos entre dois conjuntos são precisamente todas as funções do primeiro conjunto ao segundo. Portanto, nossa função de comprimento f
aqui é um morfismo do conjunto de cordas para o conjunto de números.
Como consideramos apenas a categoria definida, os Functors relevantes a partir dela são mapas que enviam objetos a objetos e morfismos a morfismos, que satisfazem certas leis algébricas.
Exemplo: Array
Array
pode significar muitas coisas, mas apenas uma coisa é um Functor - a construção de tipo, mapeando um tipo a
no tipo [a]
de todas as matrizes do tipo a
. Por exemplo, o Array
functor mapeia o tipo String
para o tipo [String]
(o conjunto de todas as matrizes de cadeias de comprimento arbitrário) e define o tipo Number
para o tipo correspondente [Number]
(o conjunto de todas as matrizes de números).
É importante não confundir o mapa do Functor
Array :: a => [a]
com um morfismo a -> [a]
. O functor simplesmente mapeia (associa) o tipo a
no tipo [a]
como uma coisa para outra. Que cada tipo é realmente um conjunto de elementos, não tem relevância aqui. Por outro lado, um morfismo é uma função real entre esses conjuntos. Por exemplo, existe um morfismo natural (função)
pure :: a -> [a]
pure = x => [x]
que envia um valor para a matriz de 1 elemento com esse valor como entrada única. Essa função não faz parte do Array
Functor! Do ponto de vista desse functor, pure
é apenas uma função como outra qualquer, nada de especial.
Por outro lado, o Array
Functor tem sua segunda parte - a parte do morfismo. Que mapeia um morfismo f :: a -> b
em um morfismo [f] :: [a] -> [b]
:
// a -> [a]
Array.map(f) = arr => arr.map(f)
Aqui arr
está qualquer matriz de comprimento arbitrário com valores do tipo a
e arr.map(f)
é a matriz do mesmo comprimento com valores do tipo b
, cujas entradas são resultados da aplicação f
às entradas de arr
. Para torná-lo um functor, as leis matemáticas do mapeamento de identidade para identidade e de composições para composições devem ser válidas, que são fáceis de verificar neste Array
exemplo.