Грубый Обзор
В функциональном программировании функтор - это, по сути, конструкция, поднимающая обычные унарные функции (т. Е. С одним аргументом) в функции между переменными новых типов. Гораздо проще писать и поддерживать простые функции между простыми объектами и использовать функторы для их подъема, а затем вручную писать функции между сложными объектами-контейнерами. Еще одним преимуществом является написание простых функций только один раз, а затем их повторное использование через различные функторы.
Примеры функторов включают массивы, функторы «возможно» и «либо», фьючерсы (см., Например, 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:
// 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типа тип bNumber и fфункцию, отображающую строку в ее длину:
// f :: String -> Number
f = str => str.length
Здесь a = Stringпредставлен набор всех строк и b = Numberнабор всех чисел. В этом смысле aи bпредставляют, и представляют объекты в категории множеств (которая тесно связана с категорией типов, причем здесь разница не существенна). В категории множеств морфизмы между двумя наборами - это точно все функции из первого множества во второе. Таким образом, наша функция длины f- это морфизм из набора строк в набор чисел.
Поскольку мы рассматриваем только заданную категорию, соответствующие Функторы из нее в себя являются картами, отправляющими объекты объектам, и морфизмы в морфизмы, которые удовлетворяют определенным алгебраическим законам.
Пример: Array
Arrayможет означать много вещей, но только одна вещь - это Functor - конструкция типа, отображающая тип aв тип [a]всех массивов типа a. Например, Arrayфунктор отображает тип String в тип [String](набор всех массивов строк произвольной длины) и устанавливает тип Numberв соответствующий тип [Number](набор всех массивов чисел).
Важно не путать карту Функтора
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примере.