Грубый Обзор
В функциональном программировании функтор - это, по сути, конструкция, поднимающая обычные унарные функции (т. Е. С одним аргументом) в функции между переменными новых типов. Гораздо проще писать и поддерживать простые функции между простыми объектами и использовать функторы для их подъема, а затем вручную писать функции между сложными объектами-контейнерами. Еще одним преимуществом является написание простых функций только один раз, а затем их повторное использование через различные функторы.
Примеры функторов включают массивы, функторы «возможно» и «либо», фьючерсы (см., Например, 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
типа тип b
Number и 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 элемента с этим значением в виде одной записи. Эта функция не является частью 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
примере.