Функтор в JavaScript

Рассмотрим следующий код:

function plus1(value) {  
    return value + 1  
}  

Это простая функция, которая получает числовое значение и добавляет к нему единицу. Аналогичным образом мы можем сделать функцию plus2. Мы будем позже использовать эти функции:

function plus2(value) {  
    return value + 2  
}  

Или мы можем написать обобщенную функцию, чтоб использовать любую из этих функций по необходимости:

function F(value, fn) {  
    return fn(value)  
}
F(1, plus1) ==>> 2

Эта функция будет работать хорошо, пока будем ей передавать целочисленное значение. Попробуем коллекцию:

F([1, 2, 3], plus1)   ==>> '1,2,31'

Упс! Мы взяли коллекцию целых чисел, добавили целое число и получили строку! Мало того, что оно сделало все неверно, так еще и вернуло строку как результат от коллекции. Другими словами, наша программа отправила входные данные в корзину. Нам нужно, чтобы F делала "правильные вещи". То есть, правильно обрабатывала структуру на протяжении всей операции.

Что значит "обрабатывала структуру"? Наша функция должна "распаковать" данную коллекцию и получить ее элементы. Потом вызвать полученную функцию с каждым элементом. После чего "запаковать" полученое значение в новую коллекцию, которую собственно и вернуть. К счастью, JavaScript уже имеет такую. Она называется map:

[1, 2, 3].map(plus1)   ==>> [2, 3, 4]

И map это функтор!

Функтор - это функция, которая получает значения и функцию, и делает "правильные вещи".

Если поточнее:

Функтор - функция, которая получает значения и функцию, распаковывает значения для получения их внутренние значение(значения), вызывает полученную функцию с внутренними значениям, запаковывает возвращенные значения в новую структуру, и впоследствии возвращает новую структуру.

Необходимо отметить, что это зависит от "типа" значения, распаковка может приводить к значению или набора значений.

Так же возвращенная структура не обязательно должна иметь тот же тип, что и оригинальная. В случает с map оба значение и возвращенное значение имеют одинаковую структуру (Коллекцию). Возвращенная структура может иметь любой тип, пока могут быть доступны индивидуальные элементы. Если у вас есть функция, которая получает Array и возвращает Object со всеми элементами, где ключ является индексом с соответствующими значениями, то она так же может быть функтором.

В случае с JavaScript, filter так же является функтором, так как возвращает коллекцию, хотя forEach им не является, так как возвращает undefined, т. е. не сохраняет структуру.

Функторы пришли из теории категорий в математике, где функторы определены как "гомоморфизм между категориями". Немного разберемся, что значат этот набор слов:

homo = одна и та же

morphisms = функции которые сохраняют структуру

category = тип

Согласно теории, функция F является функтором, когда две компонуемы обычные функции f и g:

F(f . g) = F(f) . F(g)

где . указывает на композицию, т. е. функторы должны поддерживать композицию.

Поэтому, это уравнение может быть использовано для доказательства того, является функция функтором или нет.

Фукнтор коллекций

Мы видели, что map является функтором, который работает с типом Array. Давайте докажем, что функция Array.map - это функтор:

function compose(f, g) {
    return function(x) {return f(g(x))}
}

Композиция функций - это вызов набора функций, вызов вызова следующей функции, с результатами от предыдущей. Обратите внимание, что композиция функций работает слева направо. Первой будет вызвана g, потом f:

[1, 2, 3].map(compose(plus1, plus2))   ==>> [ 4, 5, 6 ]

[1, 2, 3].map(plus2).map(plus1)        ==>> [ 4, 5, 6 ]

Да! map однозначно функтор.

Давайте попробуем несколько функторов. Вы можете написать функторы для значений любого типа, до тех пор, пока сможете распаковать значение и вернуть структуру.

Функтор строк

Итак, можем ли мы написать фукнтор строк? Можно ли распаковать строку? В действительности можно, если рассматривать строку как коллекцию символов. Т. е. на деле все зависит от того, под каким углом смотреть на значение. Мы так же знаем, что символы имеют коды, которые на деле являются числом. Поэтому, вы выполним plus1 на каждом коде символа, и запакуем обратно в строку:

function stringFunctor(value, fn) {  
    var chars = value.split("")  
    return chars.map(function(char) {  
        return String.fromCharCode(fn(char.charCodeAt(0)))  
    }).join("")  
}

stringFunctor("ABCD", plus1) ==>> "BCDE"  

Вы уже можете видеть, насколько функторы хороши. Вы можете написать синтаксический анализатор, используя в основе функтор.

Функтор функции

В JavaScript функции полноправные жители (first class citizens). Это значит, что вы можете рассматривать функции как любое другое значение. Итак, можем ли мы написать функтор для типа, где значение является функцией? Мы должны! Но как же распаковать функцию? Можем распаковать ее, просто вызвав, и получить ее значение. Но мы тут же столкнемся с проблемой. Для вызова функции, нам необходимы ее аргументы. Запомните, что функтор - это функция, которая приходит к нам как значение. Мы можем решить эту дилемму, заставив функтор вернуть новую функцию. Эта функция вызывается с аргументами, а мы, в свою очередь, вызовем функцию значения с аргументом, после вызываем оригинальную функцию-функтор с возвращенным значением!

function functionFunctor(value, fn) {  
    return function(initial) {  
        return function() {  
            return fn(value(initial))  
        }  
    }  
}

var init = functionFunctor(function(x) {return x * x}, plus1)  
var final = init(2)  
final() ==> 5

Наш функтор функции, на самом деле, не очень-то много и делает. Но есть несколько моментов, которые необходимо отметить. Ничего не произойдет, пока вы не вызовете final. Все находится в состоянии анабиоза, пока вы не вызовете final. Функтор функции - это база для других функциональных штук, например, управление состоянием, непрерывный вызов и даже обещания. Вы можете написать свой функтор для реализации этих штук.

Функтор MayBe

function mayBe(value, fn) {
    return value === null || value === undefined ? value : fn(value)
}

Да, это правильный функтор.

mayBe(undefined, compose(plus1, plus2))     ==>> undefined
mayBe(mayBe(undefined, plus2), plus1)       ==>> undefined
mayBe(1, compose(plus1, plus2))             ==>> 4
mayBe(mayBe(1, plus2), plus1)               ==>> 4

Итак mayBe - это функтор тестирования. Здесь нет необходимости распаковывания или запаковывания. Он просто ничего не возвращает, если ничего не передать. mayBe полезен для короткой проверки на неопределенность значения; можно его использовать как замену для следующего кода:

if (result === null) {
    return null
} else {
    return doSomething(result)
}

Функция Идентичности

function id(x) {
    return x
}

Функция выше известная как функция идентичности. Это просто функция, которая возвращает значение, передаваемое ей. Она так называется потому, что отображает композицию идентичности в математике. Выше мы изучили, что функторы должны поддерживать композицию. Однако, я все же не упомянул то, что функторы так же должны поддерживать идентичность, например:

F(value, id) = value

Давайте проверим это на map.

[1, 2, 3].map(id)    ==>>  [ 1, 2, 3 ]

Сигнатура типа

Сигнатура функции - это тип ее аргументов и возвращаемого значения. Итак, сигнатура функции plus:

f: int -> int

Сигнатура функтора map зависит от сигнатуры аргумент-функции. Если map была вызвана с plus1, тогда ее сигнатура будет:

map: [int] -> [int]

Хотя сигнатура этой функции не обязательно должна быть как указано выше. У нас могла быть следующая функция:

f: int -> string

в этом случае сигнатура map может быть:

map: [int] -> [string]

Единственное ограничение - это то, что смена типа не влияет на компонуемость функтора. В общем, сигнатура функтора может быть:

F: A -> B

Другими словами, map может получить коллекцию целых чисел и вернуть коллекцию строк и при том все же быть функтором.

Монады - это специальный тип функторов, сигнатура которых:

M: A -> A

Подробнее о монадах в следующей главе.

Источник

2 thoughts on “Функтор в JavaScript

Добавить комментарий