Грубый обзор
В функциональном программировании функтор , по сути, представляет собой конструкцию, в которой обычные унарные функции (т. Е. С одним аргументом) функции между переменными новых типов. Гораздо проще писать и поддерживать простые функции между простыми объектами и использовать функторы для их подъема, а затем вручную писать функции между сложными объектами-контейнерами. Еще одним преимуществом является написание простых функций только один раз, а затем их повторное использование через различные функторы.
Примеры функторов включают массивы, функторы «возможно» и «либо», фьючерсы (см., Например, 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()
Теория категорий
A Функтор в Теория категорий представляет собой карту между двумя категориями, соблюдающими композицию их морфизмов. В Компьютерный язык основной интересующей категорией является та, чьи объекты имеют типы (определенные наборы значений) и чьи морфизмы являются функциями 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
может означать много вещей, но только одна вещь - это Functor - конструкция типа, отображающая тип a
в тип [a]
всех массивов типа a
. Например, функтор Array
отображает тип String
на тип [String]
(набор всех массивов строк произвольной длины) и устанавливает тип Number
на соответствующий тип [Number]
(набор все массивы чисел).
Важно не путать карту Функтора
Array :: a => [a]
с морфизмом a -> [a]
. Функтор просто отображает (связывает) тип a
в тип [a]
как одно в другое. То, что каждый тип на самом деле представляет собой набор элементов, здесь не имеет значения. Напротив, морфизм является действительной функцией между этими наборами. Например, существует естественный морфизм (функция)
pure :: a -> [a]
pure = x => [x]
, который отправляет значение в массив из 1 элемента с этим значением в виде одной записи. Эта функция не является частью Array
Функтора! С точки зрения этого функтора, 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
.