Szorstki przegląd
W programowaniu funkcjonalnym funktor jest zasadniczo konstrukcją podnoszenia zwykłych funkcji jednoargumentowych (tj. Tych z jednym argumentem) do funkcji między zmiennymi nowych typów. Znacznie łatwiej jest pisać i utrzymywać proste funkcje między prostymi obiektami i używać funktorów do ich podnoszenia, a następnie ręcznie pisać funkcje między skomplikowanymi obiektami kontenerowymi. Kolejną zaletą jest napisanie prostych funkcji tylko raz, a następnie ponowne użycie ich za pomocą różnych funktorów.
Przykłady funktorów obejmują tablice, funty „może” i „albo”, futures (patrz np. Https://github.com/Avaq/Fluture ) i wiele innych.
Ilustracja
Rozważ funkcję konstruującą pełne imię i nazwisko osoby na podstawie imienia i nazwiska. Możemy to zdefiniować fullName(firstName, lastName)
jako funkcję dwóch argumentów, co jednak nie byłoby odpowiednie dla funktorów, które zajmują się funkcjami tylko jednego argumentu. Aby temu zaradzić, zbieramy wszystkie argumenty w jednym obiekcie name
, który teraz staje się pojedynczym argumentem funkcji:
// In JavaScript notation
fullName = name => name.firstName + ' ' + name.lastName
Co teraz, jeśli mamy wiele osób w jednym szeregu? Zamiast ręcznie przeglądać listę, możemy po prostu ponownie użyć naszej funkcji fullName
za pomocą map
metody przewidzianej dla tablic z krótkim pojedynczym wierszem kodu:
fullNameList = nameList => nameList.map(fullName)
i używaj go jak
nameList = [
{firstName: 'Steve', lastName: 'Jobs'},
{firstName: 'Bill', lastName: 'Gates'}
]
fullNames = fullNameList(nameList)
// => ['Steve Jobs', 'Bill Gates']
Będzie to działać, ilekroć każdy wpis w naszym nameList
obiekcie jest obiektem zapewniającym zarówno właściwości, jak firstName
i lastName
właściwości. Ale co, jeśli niektóre obiekty nie (a nawet wcale nie są)? Aby uniknąć błędów i uczynić kod bezpieczniejszym, możemy owinąć nasze obiekty w Maybe
typ (patrz np. 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()
gdzie Just(name)
jest pojemnikiem zawierającym tylko prawidłowe nazwy i Nothing()
jest specjalną wartością używaną do wszystkiego innego. Teraz zamiast przerywać (lub zapominać) sprawdzanie poprawności naszych argumentów, możemy po prostu ponownie użyć (podnieść) naszą oryginalną fullName
funkcję z innym pojedynczym wierszem kodu, ponownie opartym na map
metodzie, tym razem przewidzianym dla typu Może:
// Maybe Object -> Maybe String
maybeFullName = maybeName => maybeName.map(fullName)
i używaj go jak
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 kategorii
Funktora w teorii kategorii znajduje się mapa dwie kategorie poszanowaniem skład swoich morfizmów. W języku komputerowym główną kategorią zainteresowań jest ta, której obiektami są typy (określone zestawy wartości) i której morfizmy są funkcjami f:a->b
jednego typu a
na inny b
.
Na przykład, weźmy a
się za String
typ,b
Liczba i f
jest funkcją odwzorowującą ciąg znaków na jego długość:
// f :: String -> Number
f = str => str.length
Tutaj a = String
reprezentuje zestaw wszystkich ciągów i b = Number
zestaw wszystkich liczb. W tym sensie, jak a
i b
reprezentacji obiektów w Set Kategorii (co jest ściśle związane z kategorią typów, z tą różnicą, nieistotne tutaj). W kategorii zestawu morfizmy między dwoma zestawami są dokładnie wszystkimi funkcjami od pierwszego zestawu do drugiego. Zatem naszą funkcją długości f
jest morfizm ze zbioru ciągów na zbiór liczb.
Ponieważ bierzemy pod uwagę tylko zestaw kategorii, odpowiedni funktorami z niej samych są mapy wysyłające obiekty do obiektów i morfizmy do morfizmów, które spełniają pewne prawa algebraiczne.
Przykład: Array
Array
może oznaczać wiele rzeczy, ale tylko jedna rzecz to Functor - konstrukcja typu, mapująca typ a
na typ [a]
wszystkich tablic typu a
. Na przykład Array
funktor odwzorowuje typ String
na typ [String]
(zbiór wszystkich tablic ciągów o dowolnej długości), a zestaw typ Number
na odpowiedni typ [Number]
(zbiór wszystkich tablic liczb).
Ważne jest, aby nie mylić mapy Functor
Array :: a => [a]
z morfizmem a -> [a]
. Funktor po prostu odwzorowuje (kojarzy) typ a
na typ [a]
jako jedna rzecz na drugą. To, że każdy typ jest w rzeczywistości zestawem elementów, nie ma tutaj znaczenia. Natomiast morfizm jest faktyczną funkcją między tymi zbiorami. Na przykład występuje naturalny morfizm (funkcja)
pure :: a -> [a]
pure = x => [x]
który wysyła wartość do tablicy 1-elementowej z tą wartością jako pojedynczy wpis. Ta funkcja nie jest częściąArray
Functora! Z punktu widzenia tego funktora pure
jest to jak każda inna funkcja, nic specjalnego.
Z drugiej strony Array
Functor ma swoją drugą część - część morfizmu. Który f :: a -> b
zamienia morfizm w morfizm [f] :: [a] -> [b]
:
// a -> [a]
Array.map(f) = arr => arr.map(f)
Oto arr
dowolna tablica o dowolnej długości z wartościami typu a
i arr.map(f)
jest tablicą o tej samej długości z wartościami typu b
, których wpisy są wynikiem zastosowania f
do wpisów z arr
. Aby uczynić go funktorem, muszą obowiązywać matematyczne prawa odwzorowywania tożsamości na tożsamość i kompozycji na kompozycje, które łatwo sprawdzić w tym Array
przykładzie.