Это моя попытка объяснить концепцию функционального программирования, называемую «функтором», простым для понимания способом. Я объясню это на JavaScript.

Я надеюсь, что каждый, кто читает это, сможет следить за мной. Если нет, пожалуйста, оставьте мне несколько заметок! :)

Согласно Haskell и спецификации Fantasy Land, функтор - это просто что-то, что может быть отображено. На языке ООП мы бы назвали это Mappable.

Вероятно, вы уже использовали некоторые функторы (возможно, не зная, что это функтор). Некоторые из вас могут сказать: «Эй, я знаю, что можно нанести на карту. Можно сопоставить массив - вы можете сопоставить функцию с массивом! Смотреть:"

console.log([ 2, 4, 6 ].map(x => x + 3))
// => [ 5, 7, 9 ]

Ты прав! Это означает, что массив является функтором! (На данном этапе, если вы не понимаете, что такое карта, пожалуйста, сначала посмотрите это видео.)

Фактически, мы можем создать функтор из (в большинстве случаев) любого значения. Даже отдельные значения, строки и обычные объекты. Даже функции. Мы увидим, как это сделать, в этой статье.

Что вы имеете в виду под «чем-то, что можно нанести на карту»?

Сначала давайте определим "что-то": "что-то" - это просто набор значений, расположенных в некоторой форме. (и при некоторых ограничениях).

Массив - это набор (или коллекция) значений, потому что это список значений (это должно быть для вас довольно очевидно).

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

Вот класс Wrapper, экземпляр которого просто обертывает одно значение:

class Wrapper {
  constructor (value) {
    this.value = value
  }
}

Какой глупый класс. Экземпляр этого класса Wrapper также является набором значений. Вы должны думать об этом как об особом типе набора, который может содержать только одно значение - ни больше, ни меньше. Это ограничение.

Технически это называется алгебра (не так ли?), Но мы не будем использовать этот термин, потому что он звучит слишком занудно. Мы будем придерживаться слова что-нибудь.

Теперь давайте определим 'который может быть отображен': Это означает, что вы можете взять все значения внутри него, а затем для каждого значения вы получите что-то из него ( вызывая функцию). Затем вы помещаете полученные значения обратно в новый контейнер той же структуры и формы.

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

Мы рассмотрим несколько примеров, чтобы увидеть, как можно сопоставить функции с вещами.

Множество

Эта картинка из статьи Функторы, аппликативы и монады в картинках, написанная Адитьей Бхаргавой, показывает, как можно отобразить массив. (Кстати, эта статья действительно хороша.)

Есть еще правила, но давайте обсудим это позже.

Как в коде сопоставить функцию с массивом? Мы вызываем метод .map (f) и предоставляем ему функцию.

Вот как вы должны сопоставить функцию с вещами в JavaScript, в соответствии со спецификацией Fantasy Land.

Попробуем это с массивами.

const addThree = (x) => x + 3
const array = [ 2, 4, 6 ]
const mappedArray = array.map(addThree)
console.log(mappedArray)
// => [ 5, 7, 9 ]

Поскольку массив - это то, что можно отобразить, это функтор.

Одно значение

Помните тот класс Wrapper, который содержит одно значение?

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

Мы расширим Wrapper методом .map (f). Теперь это функтор.

class Wrapper {
  constructor (value) {
    this.value = value
  }
  map (f) {
    return new Wrapper(f(this.value))
  }
}

Давай попробуем:

const something = new Wrapper(39)
console.log(something)
// => { value: 39 }
const mappedSomething = something.map(addThree)
console.log(mappedSomething)
// => { value: 42 }

Надеюсь, теперь вы понимаете, почему экземпляры класса Wrapper являются функтором.

Объект

Мы можем думать о простых объектах в JavaScript как о наборе значений. Каждое значение идентифицируется некоторым ключом.

Теперь для обычных объектов JavaScript вы не можете просто сопоставить функцию с ними, потому что у объектов JavaScript обычно нет метода .map (f).

Нам нужно создать оболочку / функтор, которая позволяет отображать каждое значение внутри этого обернутого объекта. Мы назовем это ValueMappable.

class ValueMappable {
  constructor (object) {
    this.object = object
  }
  map (f) {
    const mapped = { }
    for (const key of Object.keys(this.object)) {
      mapped[key] = f(this.object[key])
    }
    return new ValueMappable(mapped)
  }
}

Вот что он делает: он принимает каждое значение внутри обернутого объекта, применяет к нему функцию и помещает его в новую оболочку, которая обертывает объект той же формы.

Давай попробуем:

const myData = { myAge: 22, friendAge: 21 }
const myDataFunctor = new ValueMappable(myData)
const threeYearsLaterFunctor = myDataFunctor.map(addThree)
const threeYearsLater = threeYearsLaterFunctor.object
console.log(threeYearsLater)
// => { myAge: 25, friendAge: 24 }

Нить

Как насчет строки? Мы также можем думать об этом как о упорядоченной последовательности кодов символов *. Это означает, что его можно нанести на карту!

К сожалению, String не предоставляет метод карты. Так что в JavaScript по умолчанию это не функтор.

(*) Также обратите внимание, что существует более одного способа представить строку как набор значений. Вы можете думать об этом как о последовательности 16-битных кодов символов (charCode). Вы также можете увидеть это как последовательность кодовых точек Unicode (codePoint). Или, может быть, последовательность байтовых значений в кодировке UTF-8.

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

class CharCodeMappable {
  constructor (string) {
    this.string = string
  }
  map (f) {
    let string = this.string
    let result = ''
    for (let i = 0; i < string.length; i++) {
      result += String.fromCharCode(
        f(string.charCodeAt(i))
      )
    }
    return new CharCodeMappable(result)
  }
}

Давай попробуем!

const mySecretString = 'Ebiil)`^bp^o'
const mySecretStringFunctor = new CharCodeMappable(mySecretString)
const decodedStringFunctor = mySecretStringFunctor.map(addThree)
const decodedString = decodedStringFunctor.string
console.log(decodedString)
// => "Hello,caesar"

Изменить: я был неправ.
Оказывается, String / CharCodeMappable НЕ является функтором.
См. этот ответ для объяснения и обсуждения .

Короче говоря, функтор, который содержит значения типа A, при отображении с функцией, которая принимает значение типа A и возвращает значение типа B, результатом должен быть функтор, содержащий значения типа B.

Это не относится к приведенному выше примеру, потому что только CharCodeMappable (концептуально) содержит число. Вы можете использовать .map (x = ›String (x)) над массивом чисел, но не над CharCodeMappable!

Функция

Когда я впервые узнал, что функция на самом деле является функтором (Я научился этому из Learn You a Haskell for Great Good!), это меня сбило с толку.

Функция - это функтор? Функторцепция?!?
Что здесь происходит?

Обратите внимание, что мы говорим о чистых функциях. Как мы можем рассматривать функцию как набор значений?

Попробуем на примере. Вот квадратная функция:

const square = (x) => x * x

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

Например, значение 4 находится в этом наборе, потому что вы можете получить его, вызвав квадрат (2) или квадрат (-2). Он также имеет значение 100, потому что вы можете получить его, вызвав квадрат (10) или квадрат (-10). NaN также есть в этом наборе.

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

Теперь представьте, что я взял все числа внутри этого набора и сложил каждое число на 3 (тем самым сопоставив эту функцию / функтор с addThree, другой функцией). Что я получу обратно?

Вы получите набор чисел не менее трех:

И это действительно возможно сделать в коде. Мы просто не применяем сопоставление заранее; мы применяем его на лету:

const squareMapped = (x) => addThree(x * x)

Внимательные читатели заметят, что отображение функции на другую функцию - это просто композиция функции:

square.map (addThree) ≡ (addThree ∘ square)
≡ ((x) = ›addThree (square (x)).

В качестве примера я буду очень непослушным и просто реализую Function.prototype.map:

Function.prototype.map = function (f) {
  const g = this
  return function () {
    return f(g.apply(this, arguments))
  }
}

Давай попробуем:

const squareMapped = square.map(addThree)
console.log(squareMapped(2))
// => 7
console.log(squareMapped(10))
// => 103

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

Теперь вы видите, как функция может стать функтором.

Шаблон наблюдателя

Шаблон наблюдателя - популярный шаблон проектирования в мире ООП.

По сути, субъект открыт для подписки других объектов (называемых наблюдателями). Когда происходит какое-то интересное событие, субъект уведомляет все наблюдающие стороны, передавая некоторые полезные данные.

Например, субъект может подключиться к потоку Twitter и уведомить наблюдателей, которые подписались на него, когда кто-то следит за вашими твитами.

tweets.subscribe(function (tweet) {
  console.log(`@${tweet.user.screen_name}: ${tweet.text}`)
})
// (At 10 am:) @dtinth: Good morning!
// (At 1 pm:)  @dtinth: Good afternoon!
// (At 11 pm:) @dtinth: Good night!

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

Допустим, нас интересует только длина твита. У нас есть функция, которая принимает длину твита.

const lengthOfTweet = (tweet) => tweet.text.length

Предполагая, что этот объект является функтором, мы сможем это сделать:

tweets.map(lengthOfTweet).subscribe(function (length) {
  console.log(`Someone posted a ${length}-character tweet!`)
})
// (At 10 am:) Someone posted a 13-character tweet!
// (At 1 pm:)  Someone posted a 15-character tweet!
// (At 11 pm:) Someone posted a 11-character tweet!

Это одна из идей, лежащих в основе библиотек функционального реактивного программирования, таких как RxJS и Bacon.js.

Законы функторов

Конечно, метод .map (f) не может быть просто какой-то случайной функцией, а затем вы вызываете любой объект с помощью .map (f ) метод функтора!

Существуют правила поведения .map (f), чтобы его можно было квалифицировать как функтор.

Закон тождества:
functor.map (x = ›x) ≡ функтор

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

console.log([ 0, 1, 2, 3 ].map(x => x))
// =>       [ 0, 1, 2, 3 ]

Теперь посмотрим на второй закон.

Закон композиции:
functor.map (x = ›f (g (x))) ≡ functor.map (g) .map (f)

Learn You a Haskell for Great Good! очень хорошо сформулировал этот закон простым английским языком, поэтому я просто процитирую их здесь.

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

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

Допустим, у меня есть такие данные:

const ceos = {
  Apple:     { firstName: 'Tim', lastName: 'Cook' },
  Microsoft: { firstName: 'Satya', lastName: 'Nadella' },
  Google:    { firstName: 'Sundar', lastName: 'Pichai' }
}

У меня есть функция, которая принимает объект человека и возвращает полное имя:

const fullNameOfPerson = (person) => (
  person.firstName + ' ' + person.lastName
)

У меня есть еще одна функция, которая принимает имя и генерирует приветствие:

const greetingForName = (name) => `Hello, ${name}!`

У меня также есть функция, которая делает и то, и другое:

const greetingForPerson = (person) => (
  greetingForName(fullNameOfPerson(person))
)

Сначала я сопоставлю эти данные, чтобы получить их полные имена. Затем я снова наведу на получившийся объект, чтобы создать приветствие.

const greetings1 = (new ValueMappable(ceos)
  .map(fullNameOfPerson)
  .map(greetingForName)
  .object
)
console.log(greetings1)
// => { Apple:     'Hello, Tim Cook!',
//      Microsoft: 'Hello, Satya Nadella!',
//      Google:    'Hello, Sundar Pichai!' }

Затем я снова собираюсь сопоставить эти данные, но на этот раз я собираюсь сделать и то, и другое одновременно.

const greetings2 = (new ValueMappable(ceos)
  .map(greetingForPerson)
  .object
)
console.log(greetings2)
// => { Apple:     'Hello, Tim Cook!',
//      Microsoft: 'Hello, Satya Nadella!',
//      Google:    'Hello, Sundar Pichai!' }

Это означает, что вы можете свободно составлять и группировать вызовы .map (f) по своему усмотрению!

Резюме

Мы узнали, что функтор представляет собой набор вещей (в некоторой структуре), которые можно отобразить для получения набора новых вещей в рамках той же самой структуры.

Мы используем метод .map (f) для выполнения этого сопоставления. Массив - один из популярных функторов.

К сожалению, не многие вещи в JavaScript предоставляют метод .map (f), когда его можно отчасти сопоставить. Чтобы сделать из него функтор, нам нужно либо расширить этот тип, либо создать оболочку, которая действует как функтор.

Функторы могут быть полезны, потому что нам не нужно заботиться о его структуре. Нас интересует только то, что мы можем отобразить на нем некоторую функцию. Это напоминает вам о полиморфизме?

В любом случае, я надеюсь, что вы лучше понимаете функторы и отображение чего-то другого, кроме массива. Спасибо за прочтение! :)

использованная литература

Обновление: на основе ответов я добавил больше ссылок.

Приложение I: Другие определения

Как и в случае со многими терминами программирования, разные программисты склонны связывать разные значения с одним и тем же термином.

Раньше мне приходилось сталкиваться с различными определениями паттерна модель-представление-контроллер, где каждый интерпретировал каждый термин по-своему. Около двух месяцев назад я также размышлял о различных определениях наследования.

В этой статье я расскажу о функторах в том виде, в каком они интерпретируются Haskell, теорией категорий и спецификацией Fantasy Land. Но я также нашел несколько других определений для функторов:

Кажется, что слово функтор теряет свое первоначальное значение.

Приложение II: Продолжение