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 fullName
tramite il map
metodo 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 firstName
e lastName
proprietà. 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 Maybe
tipo (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 fullName
funzione originale con un'altra singola riga di codice, basata di nuovo sul map
metodo, 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->b
da un tipo a
a un altro b
.
Ad esempio, prendi a
per essere il String
tipo, b
il tipo Numero, ed f
è la funzione che mappa una stringa nella sua lunghezza:
// f :: String -> Number
f = str => str.length
Qui a = String
rappresenta l'insieme di tutte le stringhe e b = Number
l'insieme di tutti i numeri. In tal senso, entrambi a
e b
rappresentano 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 f
qui è 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
Array
può significare molte cose, ma solo una cosa è un Functor: il costrutto type, che associa un tipo a
al tipo [a]
di tutte le matrici di tipo a
. Ad esempio, il funzione Array
associa il tipo String
al tipo [String]
(l'insieme di tutte le matrici di stringhe di lunghezza arbitraria) e imposta il tipo Number
nel 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 a
nel 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 Array
Functor! Dal punto di vista di questo funzione, pure
è solo una funzione come qualsiasi altra, niente di speciale.
D'altra parte, il Array
Functor ha la sua seconda parte - la parte del morfismo. Che mappa un morfismo f :: a -> b
in un morfismo [f] :: [a] -> [b]
:
// a -> [a]
Array.map(f) = arr => arr.map(f)
Ecco arr
una 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 f
delle 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 Array
esempio.