Приблизний огляд
У функціональному програмуванні функтор по суті є конструкцією підйому звичайних одинарних функцій (тобто тих, що мають один аргумент) до функцій між змінними нових типів. Набагато простіше писати та підтримувати прості функції між простими об'єктами та використовувати функтори для їх підняття, а потім для ручного запису функцій між складними об'єктами контейнерів. Подальша перевага полягає в тому, щоб записати прості функції лише один раз, а потім повторно використовувати їх через різні функтори.
Приклади функторів включають масиви, "можливо" та "або" функтори, ф'ючерси (див., Наприклад, 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-елементний масив із цим значенням як єдиний запис. Ця функція не є частиною Array
Functor! З точки зору цього функтора, 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
прикладі.