Panoramica generale
Nella programmazione funzionale, un funtore è essenzialmente una costruzione di sollevamento ordinarie unari funzioni (cioè quelli con un argomento) per funzioni tra variabili dei nuovi tipi. È molto più facile scrivere e mantenere semplici funzioni tra oggetti semplici e utilizzare i funzioni per sollevarli, quindi scrivere manualmente funzioni tra oggetti container complicati. Un ulteriore vantaggio è quello di scrivere funzioni semplici una sola volta e poi riutilizzarle tramite diversi funzioni.
Esempi di funzioni includono array, funzioni "forse" e "entrambi", futures (vedere ad esempio https://github.com/Avaq/Fluture ) e molti altri.
Illustrazione
Considera la funzione che costruisce il nome completo della persona dal nome e dal cognome. Potremmo definirlo fullName(firstName, lastName)come una funzione di due argomenti, che tuttavia non sarebbe adatto a funzioni che trattano solo le funzioni di un argomento. Per rimediare, raccogliamo tutti gli argomenti in un singolo oggetto name, che ora diventa il singolo argomento della funzione:
// In JavaScript notation
fullName = name => name.firstName + ' ' + name.lastName
E se avessimo molte persone in un array? Invece di consultare manualmente l'elenco, possiamo semplicemente riutilizzare la nostra funzione fullNametramite il mapmetodo fornito per gli array con una breve riga di codice:
fullNameList = nameList => nameList.map(fullName)
e usalo come
nameList = [
{firstName: 'Steve', lastName: 'Jobs'},
{firstName: 'Bill', lastName: 'Gates'}
]
fullNames = fullNameList(nameList)
// => ['Steve Jobs', 'Bill Gates']
Funzionerà, ogni volta che ogni voce nel nostro nameListè un oggetto che fornisce sia firstNamee lastNameproprietà. Ma cosa succede se alcuni oggetti non lo fanno (o addirittura non lo sono affatto)? Per evitare errori e rendere il codice più sicuro, possiamo inserire i nostri oggetti nel Maybetipo (ad es. 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()
dove Just(name)è un contenitore che porta solo nomi validi ed Nothing()è il valore speciale utilizzato per tutto il resto. Ora invece di interrompere (o dimenticare) per verificare la validità dei nostri argomenti, possiamo semplicemente riutilizzare (sollevare) la nostra fullNamefunzione originale con un'altra singola riga di codice, basata di nuovo sul mapmetodo, questa volta fornito per il tipo Maybe:
// Maybe Object -> Maybe String
maybeFullName = maybeName => maybeName.map(fullName)
e usalo come
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 di categoria
Una teoria della teoria delle categorie è una mappa tra due categorie che rispetta la composizione dei loro morfismi. In un linguaggio informatico , la principale categoria di interesse è quella i cui oggetti sono tipi (determinati insiemi di valori) e i cui morfismi sono funzioni f:a->bda un tipo aa un altro b.
Ad esempio, prendi aper essere il Stringtipo, bil tipo Numero, ed fè la funzione che mappa una stringa nella sua lunghezza:
// f :: String -> Number
f = str => str.length
Qui a = Stringrappresenta l'insieme di tutte le stringhe e b = Numberl'insieme di tutti i numeri. In tal senso, entrambi ae brappresentano oggetti nella categoria Set (che è strettamente correlata alla categoria dei tipi, con la differenza che qui è inessenziale). Nella categoria Set, i morfismi tra due set sono precisamente tutte le funzioni dal primo al secondo. Quindi la nostra funzione di lunghezza fqui è un morfismo dall'insieme delle stringhe nell'insieme dei numeri.
Considerando solo la categoria impostata, i fattori rilevanti da essa stessi in sé sono mappe che inviano oggetti a oggetti e morfismi a morfismi, che soddisfano determinate leggi algebriche.
Esempio: Array
Arraypuò significare molte cose, ma solo una cosa è un Functor: il costrutto type, che associa un tipo aal tipo [a]di tutte le matrici di tipo a. Ad esempio, il funzione Arrayassocia il tipo String al tipo [String](l'insieme di tutte le matrici di stringhe di lunghezza arbitraria) e imposta il tipo Numbernel tipo corrispondente [Number](l'insieme di tutte le matrici di numeri).
È importante non confondere la mappa di Functor
Array :: a => [a]
con un morfismo a -> [a]. Il funzione semplicemente mappa (associa) il tipo anel tipo [a]come una cosa all'altra. Che ogni tipo sia in realtà un insieme di elementi, non ha rilevanza qui. Al contrario, un morfismo è una funzione reale tra questi insiemi. Ad esempio, esiste un morfismo naturale (funzione)
pure :: a -> [a]
pure = x => [x]
che invia un valore nell'array a 1 elemento con quel valore come singola voce. Quella funzione non fa parte del ArrayFunctor! Dal punto di vista di questo funzione, pureè solo una funzione come qualsiasi altra, niente di speciale.
D'altra parte, il ArrayFunctor ha la sua seconda parte - la parte del morfismo. Che mappa un morfismo f :: a -> bin un morfismo [f] :: [a] -> [b]:
// a -> [a]
Array.map(f) = arr => arr.map(f)
Ecco arruna matrice di lunghezza arbitraria con valori di tipo a, ed arr.map(f)è una matrice della stessa lunghezza con valori di tipo b, le cui voci sono i risultati dell'applicazione fdelle voci di arr. Per renderlo un funzione, le leggi matematiche di mappare identità su identità e composizioni su composizioni devono valere, che sono facili da verificare in questo Arrayesempio.