Приблизний огляд
У функціональному програмуванні функтор по суті є конструкцією підйому звичайних одинарних функцій (тобто тих, що мають один аргумент) до функцій між змінними нових типів. Набагато простіше писати та підтримувати прості функції між простими об'єктами та використовувати функтори для їх підняття, а потім для ручного запису функцій між складними об'єктами контейнерів. Подальша перевага полягає в тому, щоб записати прості функції лише один раз, а потім повторно використовувати їх через різні функтори.
Приклади функторів включають масиви, "можливо" та "або" функтори, ф'ючерси (див., Наприклад, https://github.com/Avaq/Fluture ) та багато інших.
Ілюстрація
Розглянемо функцію, що побудує повне ім’я людини від імені та прізвища. Ми могли б визначити це як fullName(firstName, lastName)функцію двох аргументів, що, однак, не було б придатним для функціонерів, які мають справу лише з функціями одного аргументу. Для виправлення ми збираємо всі аргументи в один об’єкт name, який тепер стає єдиним аргументом функції:
// In JavaScript notation
fullName = name => name.firstName + ' ' + name.lastName
Що робити, якщо у нас у масиві багато людей? Замість того, щоб вручну перейти через список, ми можемо просто повторно використовувати нашу функцію fullNameза допомогою mapметоду, передбаченого для масивів із коротким єдиним рядком коду:
fullNameList = nameList => nameList.map(fullName)
і використовувати його як
nameList = [
{firstName: 'Steve', lastName: 'Jobs'},
{firstName: 'Bill', lastName: 'Gates'}
]
fullNames = fullNameList(nameList)
// => ['Steve Jobs', 'Bill Gates']
Це буде спрацьовувати, коли кожен запис у нашому nameListоб’єкті надає firstNameі lastNameвластивості, і властивості. Але що робити, якщо деяких об’єктів немає (або навіть взагалі не є об'єктами)? Щоб уникнути помилок і зробити код більш безпечним, ми можемо перетворити наші об'єкти у Maybeтип (наприклад, 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()
де Just(name)контейнер, що містить лише дійсні імена, і Nothing()це особливе значення, яке використовується для всього іншого. Тепер замість того, щоб перебивати (або забувати), щоб перевірити обґрунтованість наших аргументів, ми можемо просто повторно використати (підняти) свою первісну fullNameфункцію ще одним єдиним рядком коду, заснованим знову на mapметоді, цього разу передбаченому для типу "Можливо":
// Maybe Object -> Maybe String
maybeFullName = maybeName => maybeName.map(fullName)
і використовувати його як
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()
Теорія категорій
Functor в теорії категорій є відображення між двома категоріями поважають склад їх морфізм. В комп'ютерній мові основною категорією, що цікавить, є той, об'єктами якого є типи (певні набори значень), морфізми яких є функціями f:a->bвід одного типу aдо іншого b.
Наприклад, візьмемо aза Stringтип, тип bЧисло, і fце функція, що відображає рядок у його довжину:
// f :: String -> Number
f = str => str.length
Тут a = Stringпредставлений набір усіх рядків і b = Numberбезліч усіх чисел. У цьому сенсі і те, і іншеa і bпредставляють об'єкти в категорії Set (що тісно пов'язане з категорією типів, різниця тут несуттєва). У категорії «Набір» морфізми між двома множинами - це точно всі функції від першого набору до другого. Отже, наша функція довжини fтут - це морфізм від безлічі рядків у множину чисел.
Оскільки ми розглядаємо лише задану категорію, відповідними Функторами з неї є самі карти, що надсилають об'єкти та морфізми до морфізмів, які відповідають певним алгебраїчним законам.
Приклад: Array
Arrayможе означати багато речей, але лише одне - це Функтор - конструкція типу, що відображає тип aу тип [a]усіх масивів типу a. Наприклад, Arrayфунктор відображає типString у тип [String](набір усіх масивів рядків довільної довжини), а тип встановлює Numberу відповідний тип [Number](набір усіх масивів чисел).
Важливо не плутати карту Functor
Array :: a => [a]
з морфізмом a -> [a]. Функтор просто відображає (пов'язує) тип aу тип[a] як одну річ до іншої. Те, що кожен тип насправді є сукупністю елементів, тут не має жодного значення. Навпаки, морфізм є фактичною функцією між цими множинами. Наприклад, існує природний морфізм (функція)
pure :: a -> [a]
pure = x => [x]
який надсилає значення в 1-елементний масив із цим значенням як єдиний запис. Ця функція не є частиною ArrayFunctor! З точки зору цього функтора, pureце просто функція, як і будь-яка інша, нічого особливого.
З іншого боку, у ArrayФунктора є друга його частина - частина морфізму. Що відображає морфізм f :: a -> bу морфізм [f] :: [a] -> [b]:
// a -> [a]
Array.map(f) = arr => arr.map(f)
Тут arrє будь-який масив довільної довжини зі значеннями типу a, і arr.map(f)це масив однакової довжини зі значеннями типу b, записи якого є результатами застосування fдо записів arr. Щоб зробити його функціонером, повинні дотримуватися математичні закони відображення тотожності до ідентичності та композицій до композицій, які легко перевірити в цьому Arrayприкладі.