Рубрики
Без рубрики

Принципы функционального программирования

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

Автор оригинала: Yann Salmon.

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

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

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

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

Этот пост намеренно ориентирован на несколько видов читателей:

  1. Те, кто почти ничего не знает о FP, но довольно знакомы с JavaScript
  2. Те, кто со промежуточным знанием FP и некоторого знакомства с парадигмой, но кто хочет более четкую картину целого и хочет исследовать продвинутые концепции
  3. Те, кто много знает о FP и хочет обманывать + для пересмотра некоторых концепций если нужно

Я приглашаю вас осторожно обдумать каждое предложение, вместо того, чтобы спешить сквозь содержание, как мы все привыкли.

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

Просто голова, однако – этот пост не представляет собой единый источник истины, а скорее приглашение идти дальше после его прочтения.

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

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

Без дальнейшего ADO давайте начнем!

Почему функциональное программирование?

На мой взгляд, есть 3 основных преимущества FP и 3 (мало) недостатков:

Преимущества:

  1. Больше читабельности, таким образом, ремонтность
  2. Менее багги, особенно в одновременном контексте
  3. Новый способ думать о решении проблем
  4. (Личный бонус) Просто здорово узнать о!

Недостатки:

  1. Может иметь проблемы с производительностью
  2. Менее интуитивно понятен для работы при работе с государством и ввода-выводом
  3. Незнакомый для большинства людей + математическая терминология, которая замедляет процесс обучения

Теперь я объясню, почему я так думаю.

Повышенная читаемость

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

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

Кайл Симпсон фразы это так:

Поскольку мы тратим подавляющее большинство нашего чтения с чтением (около 80% случаев, когда я думаю, что) и не писать его, читабельность – это первое, что мы должны улучшить, чтобы повысить нашу эффективность при программировании.

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

Таким образом, понимание вашего Императив Код не будет такой простой, как было.

То же самое касается потенциальных коллег, которые работают с вами на проекте.

Так что читаемость – огромное преимущество для все более важных целей: ремонтопригодность.

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

К счастью, это преимущество, которое вы будете испытывать все больше, так как вы знакомы с парадигмой.

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

Теперь второй аргумент.

Менее багги код

Функциональные программы менее багги, особенно в одновременном контексте.

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

Например, представьте, что 2 потота имеют доступ к той же переменной.

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

Кроме того, рост многопроцессорных систем позволяет выполнять несколько потоков параллельно.

Так что теперь есть риск перекрытия (один поток может попытаться написать, а другой пытается прочитать).

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

Тем не менее, JavaScript является однопоточным, и мой личный опыт не расширяется за пределами этого.

Таким образом, я менее уверен в этом аргументе, но более опытные программисты, кажется, согласны с этим фактом (для того, что я слышал/прочитал).

Решение проблем

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

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

Я не говорю, что функциональное программирование всегда лучше.

Я говорю, что в некоторых случаях будет лучше, и что наличие этих знаний будет (Re) открыть ваш разум и сделать вас лучшим программистом.

Потому что теперь у вас будет больше инструментов и увеличенная мощность, чтобы выбрать правильную для проблемы под рукой.

Я даже думаю, что некоторые основные принципы в FP могут перевести на решение проблем за пределами домена компьютеров.

Давайте посмотрим недостатки сейчас.

Проблемы с производительностью

Первый в том, что, применяя методы FP, вы можете в конечном итоге использовать много времени и/или памяти.

Поскольку вы не хотите мутировать вещи, процесс в основном для копирования данных, затем мутируют это копию и используют его в качестве текущего состояния.

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

Поэтому, когда вы делаете много копий (действительно большие вложенные объекты) или используете техники, такие как рекурсион (накапливая слои в CallStack), могут появиться проблемы с производительностью.

Тем не менее, существуют многие решения (структурное обмен, оптимизация хвостовой связи), которая делает плохой производительность очень редко.

Менее интуитивно понятно

Второй недостаток – это когда вам нужны операции штата или ввода/вывода.

Ну, ты скажешь:

Я абсолютно согласен.

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

Вот так работает на самом низком уровне.

Компьютер находится в одном состоянии в данный момент, и он все время меняется.

Точка FP состоит в том, чтобы облегчить наши рассуждения по поводу кода, который увеличивает шансы, что грязные вещи, которые выходит из него, на самом деле работает.

И функциональное реактивное программирование помогает нам иметь дело с государством (если вы хотите узнать больше, есть ссылки в конце поста).

Даже если императивный код кажется легким/более интуитивным с первого взгляда, вы в конечном итоге потеряете трек. Я очень уверен, что если вы принимаете первоначальные усилия обучения FP, он окупится.

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

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

Другими словами, мы хотим сделать столько, сколько сможем в функциональном порядке и выдвинуть операции ввода/вывода на внешний слой программы:

Крутая кривая обучения

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

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

Тем не менее, эти технические/незнакомые термиры не должны заставлять вас пренебрегать очень мощными математическими принципами, которые их лежат.

Все вообще, я думаю, что сильные стороны FP перевешивают слабые стороны.

И функциональное программирование имеет большое значение для большинства программирования JavaScript общего назначения.

Просто имейте в виду, что есть несколько программ с своеобразными требованиями, для которых FP не является хорошей подходящей. Но если это не ваше дело, нет причин не использовать эту парадигку.

Теперь, если вы полтый новичок, вы можете чувствовать себя немного потерянным. Это нормально – нести со мной. Следующие разделы уточняют концепции, которые я упоминал здесь.

Теперь давайте погрузимся в орехи и болты функционального программирования.

Данные, расчеты и действия

В FP вы можете сломать свою программу в 3 частях: данные, расчеты и действия.

Данные

Данные, ну, данные. На наших языках они имеют разные формы, разные типы.

В JavaScript у вас есть цифры, строки, массивы, объекты и так далее. Но в конце дня они просто биты.

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

Тогда мы можем делать вещи с данными: расчеты или действия.

Расчеты

Расчеты являются математическими трансформациями данных.

Функции – это способ создать их. Вы предоставляете ему набор входов, и он возвращает вам набор выходов.

Вот и все.

Это ничего не делает вне функции, как в математике. Мир вокруг функции не влияет.

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

Общий термин для этого типа функции – Чистая функция Отказ

Из-за его характеристик все его поведение известно заранее. На самом деле, потому что он просто возвращает значение, мы можем рассматривать его так, как это значение, как данные.

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

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

Эти виды функций называются Первый класс Функции. В JavaScript все функции являются первым классом.

Безопасно использовать чистые функции, потому что, опять же, они как значения.

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

Так что вы можете использовать Чистые функции Как замена для Расчеты Отказ Они идентичны.

Теперь давайте поговорим о действиях.

Действия

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

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

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

Так в основном, Действия и нечистые функции одинаковы.

Вот простой пример, чтобы проиллюстрировать эти концепции:

// ↓ variable
//      ↓ data
let a = 3;

// Calculation / Pure function
const double = (x) => x * 2;

double(a);
// 6

// Action / Impure function
const IncThenPrint = () => {
  // assignment of a variable outside the scope of the function
  a = a + 1;

  // do something (here printing) outside the function
  console.log(a);
};

IncThenPrint();
// console: 4

Данные, расчеты и действия в функциональном программировании

В FP цель состоит в том, чтобы отделить данные, расчеты и действия, когда стремятся выполнять большую часть работы с расчетами.

Почему? Потому что действия полагаются на внешний мир. У нас нет полного контроля над этим.

Таким образом, мы можем получить неожиданные результаты/поведение из него. Поэтому, если большинство вашей программы сделано из действий, он быстро становится беспорядок.

Принимая предыдущий пример, что если где-то еще в программе, кто-то решил назначить объект для переменной А ?

Ну, мы получим неожиданный результат при запуске Inctingprint Потому что не имеет смысла добавлять 1 на объект:

let a = 3;

// ...
a = { key: "value" };
// ...

// Action / Impure function
const IncThenPrint = () => {
  // assignment of a variable outside the scope of the function
  a = a + 1;

  // do something (here printing) outside the function
  console.log(a);
  // prints: 4
};

IncThenPrint();
// prints: [object Object]1
// (Because JavaScript is a dynamically-typed language, it converts both operands of the + operator
// to strings so it can perform the operation, thus explaining the result.
// But obviously, that not what was intended.)

Способность дифференцировать данные, расчеты и действия в вашей программе – это фундаментальный навык для развития.

Сопоставление

Отображение – довольно тривиальная, но очень важная концепция в мире функционального программирования.

” Сопоставление от A до B “означает идти от A до B через некоторую ассоциацию.

Другими словами, указывает на B посредством некоторых связей между ними.

Например, чистая функция отображает вход на выход. Мы можем написать это так: вход -> вывод; где стрелка указывает на функцию.

Другим примером являются объекты в JavaScript. Они отображают ключи к ценностям.

На других языках эта структура данных часто называют «картой» или «хэш-картой», которая более поясна.

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

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

Больше побочных эффектов

Прежде чем мы движемся дальше, я хочу глубже в побочных эффектах в JavaScript и продемонстрировать порочную ловушку, о котором вы не можете быть в курсе.

Напомнить себя, говоря, что функция имеет побочные эффекты, то же самое, что говорит: «Когда эта функция проходит, что-то за пределами его объема будет изменяться».

Как я уже сказал, он может быть вход в консоль, что делает вызов API, изменив внешнюю переменную и т. Д.

Давайте посмотрим на пример последнего:

let y;

const f = (x) => {
  y = x * x;
};

f(5);
y; // 25

Это довольно легко понять.

Когда F Запускается, это присваивает новое значение внешней переменной y , который является побочным эффектом.

Чистая версия этого примера была бы:

const f = (x) => x * x;

const y = f(5);
// 25

Но есть еще один способ изменить внешнюю переменную, которая более тонкой:

let myArr = [1, 2, 3, { key: "value" }, "a string", 4];

const g = (arr) => {
  let total = 0;

  for (let i = 0; i < arr.length; i++) {
    if (Number.isNaN(Number(arr[i]))) {
      arr[i] = 0;
    }
    total += arr[i];
  }

  return total;
};

g(myArr);
// 10
myArr;
// [1, 2, 3, 0, 0, 4]
// Oops, all elements that were not numbers have been changed to 0 !

Это почему?

В JavaScript при назначении значения для переменной или передачи его к функции он автоматически скопирован.

Но есть различие, чтобы сделать здесь.

Примитивные значения ( NULL , undefined , строки, цифры, логические и символы) всегда назначаются/пропущены Value-Copy Отказ

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

Таким образом, в предыдущем примере стоимость передана G это соединение один, массив Myarr Отказ

Что происходит, в том, что G хранит адрес памяти Myarr в приостановить имя параметра используется в теле функции.

Другими словами, нет ценнослужителя каждого элемента в Myarr. Как вы ожидаете. Таким образом, когда вы манипулируете или изменитесь приостановить это на самом деле идет к Myarr Расположение памяти и выполните все вычисленные вами.

Так что да, осознайте эту крику.

Упражнения (набор 1)

  1. В фрагменте ниже найдите чистые функции и нечистые:
// a
const capitalizeFirst = (str) => str.charAt(0).toUpperCase() + str.slice(1);

// b
const greeting = (persons) => {
  persons.forEach((person) => {
    const fullname = `${capitalizeFirst(person.firstname)} ${capitalizeFirst(
      person.lastname
    )}`;

    console.log(`Hello ${fullname} !`);
  });
};

// c
const getLabels = async (endpoint) => {
  const res = await fetch("https://my-database-api/" + endpoint);
  const data = await res.json();
  return data.labels;
};

// d
const counter = (start, end) => {
  return start === end
    ? "End"
    : // e
      () => counter(start + 1, end);
};

2. Преобразуйте этот фрагмент в чистый (вы можете сделать более одной функции, если вы чувствуете необходимость):

const people = [
  { firstname: "Bill", lastname: "Harold", age: 54 },
  { firstname: "Ana", lastname: "Atkins", age: 42 },
  { firstname: "John", lastname: "Doe", age: 57 },
  { firstname: "Davy", lastname: "Johnson", age: 34 },
];

const parsePeople = (people) => {
  const parsedPeople = [];

  for (let i = 0; i < people.length; i++) {
    people[i].firstname = people[i].firstname.toUpperCase();
    people[i].lastname = people[i].lastname.toUpperCase();
  }

  const compareAges = (person1, person2) => person1.age - person2.age;

  return people.sort(compareAges);
};

parsePeople(people);
// [
//   {firstname: "DAVY", lastname: "JOHNSON", age: 34},
//   {firstname: "ANA", lastname: "ATKINS", age: 42},
//   {firstname: "BILL", lastname: "HAROLD", age: 54},
//   {firstname: "JOHN", lastname: "DOE", age: 57},
// ]

Проверить ответы .

Неподумность

Как будто мы рассматривали ранее, общий побочный эффект – это мутировать переменной.

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

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

Но в JavaScript это не так.

Таким образом, это больше о том, чтобы мышление «неизменность», чем реальная надежная реализация этой особенности.

Это означает, что вы будете в основном делать копии данных, на которых вы хотите работать.

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

Таким образом, при работе с объектом/массивом в функции вы должны сделать копию, а затем работать на нем.

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

Например, Array.prototype.map , Array.prototype.filter или Array.prototype.recuce не мутируют оригинальный массив.

С другой стороны, Array.prototype.reverse и Array.prototype.push мутируют оригинальный массив.

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

Это раздражает и в конечном итоге не прекрасно безопасно.

Неглубоко против глубоких копий

Поскольку ES6, легко сделать объект/массив копии с помощью распространения, Array.from () , Объект.assign () Отказ

Например:

// arrays
const fruits = ["apple", "strawberry", "banana"];
const fruitsCopy = [...fruits];
fruitsCopy[0] = "mutation";
// fruitsCopy: ['mutation', 'strawberry', 'banana']
// fruits (not mutated): ['apple', 'strawberry', 'banana']

// objects
const obj = { a: 1, b: 2, c: 3 };
const objCopy = { ...obj };
objCopy.a = "mutation";
// objCopy: {a: "mutation", b: 2, c: 3}
// obj (not mutated): {a: 1, b: 2, c: 3}
console.log(obj);
console.log(objCopy);

Это круто Но есть куча.

Разобрать массивы/объекты имеют только первый уровень, скопированный значением, также известный как неглубокий копировать.

Таким образом, все последующие уровни все еще смены:

// But with nested objects/arrays, that doesn't work
const nestedObj = { a: { b: "canBeMutated" } };
const nestedObjCopy = { ...nestedObj };
nestedObjCopy.a.b = "hasBeenMutated!";
console.log(nestedObj);
console.log(nestedObjCopy);
// nestedObjCopy: {a: {b: "hasBeenMutated!"}}}
// nestedObj (mutated): {a: {b: "hasBeenMutated!"}}

Чтобы решить эту проблему, нам нужна пользовательская функция, чтобы сделать Глубоко копии. Это Статья обсуждает несколько решений.

Вот укороченная версия пользовательской функции, предложенной в ней:

// works for arrays and objects
const deepCopy = (obj) => {
  if (typeof obj !== "object" || obj === null) {
    return obj; // Return the value if obj is not an object
  }

  // Create an array or object to hold the values
  let newObj = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    // Recursively (deep) copy for nested objects, including arrays
    newObj[key] = deepCopy(obj[key]);
  }

  return newObj;
};

const nestedObj = {
  lvl1: { lvl2: { lvl3: { lvl4: "tryToMutateMe" } } },
  b: ["tryToMutateMe"],
};
const nestedObjCopy = deepCopy(nestedObj);

nestedObjCopy.lvl1.lvl2.lvl3.lvl4 = "mutated";
nestedObjCopy.b[0] = "mutated";

console.log(nestedObj);
// { lvl1: { lvl2: { lvl3: { lvl4: "tryToMutateMe" } } }, b: ["tryToMutateMe"]}
console.log(nestedObjCopy);
// { lvl1: { lvl2: { lvl3: { lvl4: "mutated" } } }, b: ["mutated"]}

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

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

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

Очевидно, что копии не приходят без затрат.

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

В этих случаях, используя неизменные структуры данных из библиотеки, как Immutable.js Вероятно, лучшая идея.

Они используют технику под названием Структурное разделение который я упомянул, когда говорил о том, как разговаривать о том, как в этом посте снижаются нисходящие поставки FP.

Проверьте это отличное говорить Узнать больше.

Таким образом, дело с неизменными данными, на мой взгляд, вторые навыки имеют в вашем функциональном ремнем инструменте программиста.

Состав и карри

Состав

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

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

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

Композиция – это мощная идея.

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

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

Быть резервированным математическими законами, мы знаем, что все будет работать, если бы мы следим за ними.

Давайте познакомимся с некоторыми кодом, чтобы сделать вещи бетонными:

const map = (fn, arr) => arr.map(fn);

const first = (xs) => xs[0];

const formatInitial = (x) => x.toUpperCase() + ".";

const intercalate = (sep, arr) => arr.join(sep);

const employees = ["Yann", "Brigitte", "John", "William"];

const initials = intercalate("\n", map(formatInitial, map(first, employees)));
// Y.
// B.
// J.
// W.

Отель – Здесь немного гнездо.

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

С силой карта Мы, по сути, созданные функциональные возможности Первый , Flancapentianitial и Присоединяйтесь к в конечном итоге применить их на Сотрудники множество.

Довольно круто!

Но, как видите, гнездование раздражает. Это делает вещи сложнее читать.

Карри

Сгладить этот материал и сделать композицию ветером, мы должны говорить о Carrying Отказ

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

Обычно, когда мы делаем вызов функции, мы предоставляем все аргументы одновременно и вернемся к результату:

const add = (x, y) => x + y;

add(3, 7);
// 10

Но что, если мы сможем пройти только один аргумент и предоставить вторую позже?

Ну, мы можем сделать это, Currying Добавить вот так:

const add = (x) => (y) => x + y;

const addTo3 = add(3);
// (y) => 3 + y

// ...later
addTo3(7);
// 10

Это может быть полезно, если у нас пока нет всех аргументов.

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

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

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

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

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

По этой причине есть обычный помощник для карри функции ( от Kyle Simpson Book YDKJS ):

const curry = (fn, arity = fn.length) => {
  return (function nextCurried(prevArgs) {
    return function curried(...nextArgs) {
      const args = [...prevArgs, ...nextArgs];

      return args.length < arity ? nextCurried(args) : fn(...args);
    };
  })([]);
};

Карри принимает функцию и номер называется Ариность (по желанию).

Вольность функции – это количество аргументов, которые необходимо.

В случае Добавить это 2.

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

Итак, давайте рефоктором наш пример с Добавить :

const add = curry((x, y) => x + y);

const addTo3 = add(3);

addTo3(7);
// 10

Или мы все еще можем позвонить Добавить Со всеми его аргументами напрямую:

const add = curry((x, y) => x + y);

add(3, 7);
// 10

Частичное применение

На самом деле, Curried Строго означает «принимает один аргумент за раз», не более, не меньше.

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

Таким образом, Carrying – это ограниченная форма частичного применения.

Давайте посмотрим более явный пример частичного применения по сравнению с Carrying:

const listOf4 = curry((a, b, c, d) => `1. ${a}\n2. ${b}\n3. ${c}\n4. ${d}`);

// strict currying

const a = listOf4("First")("Second")("Third")("Fourth");
// or
const b = listOf4("First");
// later
const c = b("Second")("Third");
// later
const d = c("Fourth");

// partial application

const e = listOf4("First", "Second", "Third", "Fourth");
// or
const b = listOf4("First");
// later
const c = b("Second", "Third");
// later
const d = c("Fourth");

Вы видите разницу?

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

Честно говоря, это просто вопрос стиля.

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

Карри Помощник, который я представил, позволяет вам делать обоими.

Он протягивает реальное определение карри, но я предпочитаю иметь обе функции, и не любите имя Loosecurry Этот Кайл Симпсон использовал в книге. Итак, я немного обманул.

Просто удерживайте различия и осознайте, что Карри Помощники вы найдете в библиотеках, вероятно, следуют строгим определением.

Данные приходят последние

Конечная точка, которую я хочу сделать, это то, что мы обычно помещаем данные в качестве последнего аргумента.

При использовании предыдущих функций я использовал, это не очевидно, потому что все аргументы являются данными. Но посмотрите на это:

const replace = curry((regex, replacement, str) =>
  str.replace(regex, replacement)
);

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

Вы увидите, что это так, когда он составит функции.

Возьми все вместе

Сейчас воспользоваться кришими и сплющим наш вложенный двор до раньше, нам также нужен помощник для композиции.

Вы догадались, это называется составить !

const compose = (...fns) =>
  fns.reverse().reduce((fn1, fn2) => (...args) => fn2(fn1(...args)));

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

Функции применяются справа налево из-за fns.reverse () Отказ

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

Так что с нашим первоначальным примером:

const map = (fn, arr) => arr.map(fn);

const first = (xs) => xs[0];

const formatInitial = (x) => x.toUpperCase() + ".";

const intercalate = (sep, arr) => arr.join(sep);

const getInitials = compose(formatInitial, first);

const employees = ["Yann", "Brigitte", "John", "William"];

const initials = intercalate("\n", map(getInitials, employees));
// Y.
// B.
// J.
// W.

Первый и Flancapentianitial уже принять один аргумент.

Но карта и интеркалат Возьмите 2 аргумента, поэтому мы не можем включать их как в нашем составить помощник, потому что только один аргумент будет передан. В этом случае это массив, который оба принимают в качестве окончательного аргумента (помните, данные – это последнее, что будет передано).

Было бы неплохо дать карта и Интеркалат их соответствующий первый аргумент заранее.

Подожди минутку – мы можем начать их!

// ...

const map = curry((fn, arr) => arr.map(fn));

const intercalate = curry((sep, arr) => arr.join(sep));

const formatInitials = compose(
  intercalate("\n"),
  map(formatInitial),
  map(first)
);

const employees = ["Yann", "Brigitte", "John", "William"];

const initials = formatInitials(employees);
// Y.
// B.
// J.
// W.

Так чисто!

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

Итак, давайте визуализируем, что происходит, когда Flancapitianitials (сотрудники) проанализирован:

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

Тогда как с составить Я должен вернуться назад, чтобы написать следующую преобразование. Это просто ломает поток моего мышления.

К счастью, это не сложно настроить его, чтобы идти слева направо.

Мы просто должны избавиться от .Reverse () часть.

Давайте назовем наш новый помощник трубка :

const pipe = (...fns) => fns.reduce((fn1, fn2) => (...args) => f2(f1(...args)));

Так что, если мы рефикторуем предыдущий фрагмент, мы получаем:

const formatInitials = pipe(map(first), map(formatInitial), intercalate("\n"));

Для визуализации, то же самое, что и составить Но в обратном порядке:

Хинди-Милнер Тип подписи

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

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

Чтобы противостоять этому, вы перечитаете только детали, которые вам нужны. Но это может быть довольно утомительным.

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

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

Например:

// ↓ function name
//                  ↓ input
//                            ↓ output
// formatInitial :: String -> String
const formatInitial = (x) => x.toUpperCase() + ".";

Здесь мы видим, что Flancapentianitial берет Строка и возвращает Строка Отказ

Нам не волнует реализацию.

Давайте посмотрим на другой пример:

// first :: [a] -> a
const first = (xs) => xs[0];

Типы могут быть выражены переменными (обычно A , B , так далее.) И кронштейны означают «массив« все, что внутри ».

Таким образом, мы могли бы буквально читать эту подписью, как это:

Первый берет массив А и возвращает А , где А может быть любого типа.

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

Если бы вывод имел другой тип, мы бы использовали B :

// imaginaryFunction :: a -> b

Предупреждение!

Это не гарантирует, что А а также B разные типы. Они все еще могут быть одинаковыми.

Наконец, давайте посмотрим на случай Интеркалат который немного сложнее:

// intercalate :: String -> [a] -> String
const intercalate = curry((sep, arr) => arr.join(sep));

Хорошо, здесь есть 2 стрелки, которые можно заменить «возвращением …».

Они указывают на функции.

Итак, Интеркалат берет Строка Затем возвращает функцию, которая принимает массив А , который возвращает Строка Отказ

Вау, это трудно отслеживать.

Мы могли бы написать подписью, как это:

// intercalate :: String -> ([a] -> String)

Теперь очевидно, что впервые возвращает функцию, которая находится в скобках здесь. И тогда эта функция займет [A] в качестве ввода и возврата Строка Отказ

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

// imaginaryFunction :: a -> b -> c -> d -> e

// or

// imaginaryFunction :: a -> (b -> (c -> (d -> e)))

// ...you see how parens nesting affects readability

е Тип на правой стороне, является выходом.

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

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

Но это остается приятным инструментом, чтобы иметь в своем инструменте инструментов, потому что многие функциональные библиотеки используют подписи этих типов в своих документациях. И идиоматические функциональные языки (например, haskell) используют их сильно.

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

Похлопайте себя на спину, чтобы прочитать это далеко.

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

Действительно, это именно то, что мы сделали.

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

составить , труба , карта и Уменьшить все функции высшего порядка, потому что они принимают хотя бы одну функцию в качестве ввода.

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

Достаточно поразительно. Давайте получим некоторую практику.

Упражнения (набор 2)

  1. Учитывая строку формы:
const input = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

… и эти помощники:

// filter :: (a -> Boolean) -> [a] -> [a]
const filter = curry((fn, arr) => arr.filter(fn));

// removeDuplicates :: [a] -> [a]
const removeDuplicates = (arr) => Array.from(new Set(arr));

// getChars :: String -> [Character]
const getChars = (str) => str.split("");

// lowercase :: String -> String
const lowercase = (str) => str.toLowerCase();

// sort :: [a] -> [a]
const sort = (arr) => [...arr].sort();

Создать функцию getletters Это возвращает все буквы в строке без дубликатов, в алфавитном порядке, а в нижнем регистре.

Цель состоит в том, чтобы использовать составить и/или труба :

// getLetters :: String -> [Character]
const getLetters = ...

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

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

{
  "groupName": [
    {firstname: "John", lastname: "Doe", age: 35, sex: "M"},
    {firstname: "Maria", lastname: "Talinski", age: 28, sex: "F"},
    // ...
  ],
  // ...
}

Создайте функцию, которая возвращает объект формы:

{
  "groupName": {
    "medianAgeM": 34,
    "medianAgeF": 38,
  },
  // ...
}

Где Medianagem это средний возраст мужчин в группе и медианыф один из женщин.

Вот некоторые помощники:

// map :: (a -> b) -> [a] -> [b]
const map = curry((fn, arr) => arr.map(fn));

// getEntries :: Object -> [[Key, Val]]
const getEntries = (o) => Object.entries(o);

// fromEntries:: [[Key, Val]] -> Object
const fromEntries = (entries) => Object.fromEntries(entries);

// mean :: Number -> Number -> Number
const mean = curry((x, y) => Math.round((x + y) / 2));

// reduceOverVal :: (b -> a -> b) -> b -> [Key, [a]] -> [Key, b]
const reduceOverVal = curry((fn, initVal, entry) => [
  entry[0],
  entry[1].reduce(fn, initVal),
]);

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

// groupsMedianAges :: Object -> Object
const groupsMedianAges = ...

3. Найти тип подписи Уменьшить :

const reduce = curry((fn, initVal, arr) => arr.reduce(fn, initVal));

4. Найти тип подписи Карри :

const curry = (fn, arity = fn.length) => {
  return (function nextCurried(prevArgs) {
    return function curried(...nextArgs) {
      const args = [...prevArgs, ...nextArgs];

      return args.length < arity ? nextCurried(args) : fn(...args);
    };
  })([]);
};

Проверить ответы .

Работа с коробками: От функторов до монадских

Вы уже можете быть подчеркнуты по названию этого раздела. Возможно, вы думаете: «Какого черта« Функторы »и« монады »?»

Или, может быть, вы слышали о монаде, потому что они отлично «сложно», чтобы понять.

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

На самом деле, если я расскажу о них в конце этого урока, это потому, что я думаю, что они очень мощные инструменты, которые нам не нужно очень часто.

Вот обнадеживающая часть: как и все в мире, они не волшебство.

Они следуют тому же правилам физики (и более конкретно информатики и математики) как все остальное.

Так что в конце дня они понятны. Это просто требует правильного количества времени и энергии.

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

Теперь найдите эту трубку настойчивость в вашем инструменте и давайте начнем.

Зачем использовать коробки?

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

Однако как мы имеем дело с нулевой или неопределенный ? Как мы имеем дело с исключениями?

Кроме того, как нам управлять побочными эффектами, не теряя контроль, потому что однажды нам нужно будет выполнять их?

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

Обычный способ справиться с разветвлением – контрольный поток.

Однако контрольный поток является императивным. Он описывает «как» код работает.

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

Мы используем это поле в качестве ввода/вывода для функций независимо от того, что внутри.

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

Таким образом, нам не нужно адаптировать наши функции к данным. Нам не нужно беспорядок их логикой, к которой не принадлежит.

Вещи как:

const myFunc = (x) => {
  // ...
  if (x !== null) {
    // ...
  } else {
    // ...
  }
};

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

Ящики, которые мы увидим, названы Алгебраические типы данных (ADT), позвольте нам делать больше, сохраняя данные и функции отдельными.

Функторы и монады – это действительно алгебраические типы данных.

Функторы

Функторы – это контейнеры/структуры данных/типы, которые содержат данные вместе с карта метод.

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

Давайте представим Личность , самый простой функтор:

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

const Identity = (x) => ({
  inspect: () => `Identity(${x})`,
  map: (fn) => Identity(fn(x)),
  value: x,
});

// add5 :: Number -> Number
const add5 = (x) => x + 5;

const myFirstFunctor = Identity(1);

myFirstFunctor.map(add5);
// Identity(6)

Понимаете? Не так сложно!

Личность эквивалент личность функция Но в мире функторов.

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

// identity :: a -> a
const identity = (x) => x;

Это ничего не делает на данных, просто возвращает его как есть.

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

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

Личность служит той же цели, но при сочинении функторов.

Больше на этом позже.

Вернуться к предыдущему фрагменту, мы могли бы сделать карта (Add5, 1) И это дало бы нам один и тот же результат, кроме того, что там не было бы вокруг него контейнера.

Так что здесь нет дополнительной функции.

Теперь давайте посмотрим другой функтор под названием Может быть :

const Nothing = () => ({
  inspect: () => `Nothing()`,
  map: Nothing,
});

const Maybe = { Just, Nothing };

// Just is equivalent to Identity

Может быть это смесь из 2 функторов, Просто и Ничего Отказ

Ничего Содержит, ну ничего нет. Но это все еще функтор, поэтому мы можем использовать его, куда бы нам нужны функторы.

Может быть , как его название предлагает, Май Содержит значение ( просто ) или нет ( ничего ).

Теперь, как бы мы это использовали?

Большую часть времени, он используется в функциях, которые могут вернуться нулевой или неопределенный :

// isNothing :: a -> Boolean
const isNothing = (x) => x === null || x === undefined;

// safeProp :: String -> Object -> Maybe a
const safeProp = curry((prop, obj) =>
  isNothing(obj[prop]) ? Maybe.Nothing() : Maybe.Just(obj[prop])
);

const o = { a: 1 };

const a = safeProp("a", o);
// Just(1)

const b = safeProp("b", o);
// Nothing

a.map(add5);
// Just(6)

b.map(add5);
// Nothing

Вы видите, были сила Может быть ложь?

Вы можете безопасно применить функцию на внутреннее значение в любом функте SafeProp Возвращает, вы не получите неожиданные Нан Результат, потому что вы добавили номер с нулевой или undefined Отказ

Благодаря Ничего Функтор, функция сопоставлена вообще не будет вызвана.

Тем не менее, Может быть Реализации часто читят немного, делая устойчивость Проверьте внутри монада, тогда как строго чистый монад не должен:

const Maybe = (x) => ({
  map: (fn) => (x === null || x === undefined ? Maybe(x) : Maybe(fn(x))),
  inspect: () => `Maybe(${x})`,
  value: x,
});

// safeProp :: String -> Object -> Maybe a
const safeProp = curry((prop, obj) => Maybe(obj[prop]));

const o = { a: 1 };

const c = safeProp("a", o);
// Maybe(1)

const d = safeProp("b", o);
// Maybe(undefined)

c.map(add5);
// Maybe(6)

d.map(add5);
// Maybe(undefined)

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

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

При использовании Может быть В реальных случаях нам в конечном итоге нужно что-то сделать с данными для отпускания значения.

Кроме того, если операции взяли ненужную ветку и не ударуются, мы получим Ничего Отказ

Давайте представим, что мы хотим распечатать значение, полученную из o В нашем предыдущем примере.

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

Так что для выпуска стоимости и обеспечить загрязнение, если мы получим Ничего у нас есть маленький помощник позвонил Может быть :

// maybe :: c -> (a -> b) -> Maybe a -> b | c
const maybe = curry((fallbackVal, fn, maybeFunctor) =>
  maybeFunctor.val === undefined ? fallbackVal : fn(maybeFunctor.val)
);

// ...

const o = { a: 1 };

const printVal1 = pipe(
  safeProp("a"),
  maybe("Failure to retrieve the value.", add5),
  console.log
);

const printVal2 = pipe(
  safeProp("b"),
  maybe("Failure to retrieve the value.", add5),
  console.log
);

printVal1(o);
// console: 6
printVal2(o);
// console: "Failure to retrieve the value."

Большой!

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

Но на самом деле, это то, что вы уже знакомы.

Если вы знакомы с JavaScript, шансы состоят в том, что вы использовали встроенный карта :

[1, 2, 3].map((x) => x * 2);
// [2, 4, 6]

Ну, помните определение функтора. Это структура данных, которая имеет карта метод.

Теперь посмотрите на предыдущий фрагмент: какова структура данных, которая имеет карта Метод здесь?

Массив Действительно Родной Массив Тип в JavaScript – это функтор!

Его специальность состоит в том, что она может содержать несколько значений. Но сущность карта Остается то же самое: он принимает значение в качестве ввода и возврата/отображает его на вывод.

Таким образом, в этом случае функция Mapper работает для каждого значения.

Прохладный!

Теперь, когда мы знаем, что такое функтор, давайте перейдем к расширению интерфейса.

Указал

Заостренный функтор – это тот, который имеет (aka Чистый , Блок ) Метод.

Так с Может быть Это дает нам:

const Maybe = {Just, Nothing, of: Just};

предназначен для размещения заданного значения в Минимальный контекст по умолчанию из функтора.

Вы можете спросить:

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

Если мы используем Ничего , это проигнорирует все, что мы отображаем.

ожидает, что вы вставите «успешное» значение.

Таким образом, вы все еще можете снимать себя в ногу, вставляя undefined Например, а затем отображение функции, которая не ожидает этого значения:

Maybe.of(undefined).map((x) => x + 1);
// Just(NaN)

Давайте введем еще один функтор, чтобы лучше понять, когда это полезно:

const IO = (dangerousFn) => ({
  inspect: () => `IO(?)`,
  map: (fn) => IO(() => fn(dangerousFn())),
});

IO.of = (x) => IO(() => x);

В отличие от Просто , Ио Не получайте ценность как есть, но нужно, чтобы он завернул в функцию.

Это почему?

I/O обозначает Ввод/вывод Отказ

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

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

Запрос Дом является примером:

// getEl :: String -> DOM
const getEl = (sel) => document.querySelector(sel);

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

getEl("#root");
// 
// or getEl("#root"); //
There's text now !
// or getEl("#root"); // null

При вставке промежуточной функции getel Возвращает всегда тот же выход:

// getEl :: String -> _ -> DOM
const getEl = (sel) => () => document.querySelector(sel);

getEl("#root");
// function...

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

Тем не менее, мы не волшебным образом стираем эффект, потому что сейчас это возвращенная функция, нечистая.

Мы получаем чистоту из лени.

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

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

До тех пор мы можем мирно сделать наше сопоставление и состав.

Так что это механизм, используемый Ио Отказ

Если вы передаете значение непосредственно к нему, он должен быть функцией с той же подписью, что и тот, который getel Возвращает:

const a = IO(() => document.querySelector("#root"));

// and not:

const invalid = IO(document.querySelector("#root"));

Но, как вы можете себе представить, это быстро становится утомительным, чтобы всегда обернуть нашу ценность в функции, прежде чем передавать ее в Ио Отказ

Вот где Сияет – это сделает это для нас:

const betterNow = IO.of(document.querySelector("#root"));

Это то, что я имел в виду под Минимальный контекст по умолчанию Отказ

В случае Ио Это обертывает сырье в функции. Но это может быть что-то еще, это зависит от вопроса о функторе.

Упражнения (набор 3)

  1. Написать функцию верхний регистр Это заглавная строка внутри функтора:
// uppercaseF :: Functor F => F String -> F String
const uppercaseF = ...

2. Используйте верхний регистр Функция, которую вы построили ранее, Может быть и SafeProp Чтобы создать функцию, которая извлекает имя пользователя и печатает верхнюю версию его.

Объект пользователя имеет эту форму:

{
  name: "Yann Salmon",
  age: 18,
  interests: ["Programming", "Sport", "Reading", "Math"],
  // ...
}
// safeProp :: String -> Object -> Maybe a

// maybe :: c -> (a -> b) -> Maybe a -> b | c

// printUsername :: User -> _
const printUsername = ...

Проверить ответы .

Примирительные

Если вы работаете с функторами, вы будете наткнуться на ситуации, когда у вас есть несколько функтов, содержащих значения, на которых вы хотели бы применить функцию:

// concatStr :: String -> String -> String
const concatStr = curry((str1, str2) => str1 + str2);

const a = Identity("Hello");

const b = Identity(" world !");

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

Применитель Интерфейс решает эту проблему.

Функтор, который реализует это тот, который реализует AP метод. AP Занимает функтор в качестве аргумента и возвращает функтор одного типа.

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

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

Давайте продолжим наш предыдущий фрагмент, чтобы увидеть его в действии:

// concatStr :: String -> String -> String
const concatStr = curry((str1, str2) => str1 + str2);

const a = Identity("Hello");

const b = Identity(" world !");

const c = a.map(concatStr);
// Identity(concatStr("Hello", _))

const result = c.ap(b);
// Identity("Hello world !")

Во-первых, мы карта Concatstr над А Отказ Что происходит, в том, что Concatstr («Привет») называется и становится внутренним значением C , все еще Личность функтор.

И помните, что возвращается Concatstr («Привет») ? Другая функция, которая ждет оставшихся аргументов!

Действительно, Concatstr карман

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

Тогда, как я уже сказал, AP Карты ценность функтора, которое он вызывается (в этом случае c Так что это карты Concatstr («Привет») ) за ценность функтора, взятого в качестве аргумента (здесь это B , содержащие "Мир!" ).

Итак, Результат заканчивается быть Личность Функтор (тот же тип, что и B ), содержащий результат Concatstr («Hello») («Мир!») то есть "Привет, мир !" !

Вот реализация:

const Identity = (x) => ({
  inspect: () => `Identity(${x})`,
  // Functor interface
  map: (fn) => Identity(fn(x)),
  // Applicative interface
  ap: (functor) => functor.map(x),
  value: x,
});

// Pointed interface
Identity.of = (x) => Identity(x);

Как видите, функтор AP вызывается, должен содержать функцию. В противном случае это не будет работать. В нашем предыдущем примере это было C шаг.

Если мы все встроим, мы получаем:

// concatStr :: String -> String -> String
const concatStr = curry((str1, str2) => str1 + str2);

const result = Identity("Hello").map(concatStr).ap(Identity(" world !"));
// Identity("Hello world !")

Есть интересная математическая недвижимость о AP :

F(x).map(fn) === F(fn).ap(F(x));

Левая сторона равенства соответствует тому, что мы сделали ранее.

Так что следует под правой стороне, Результат также может быть написано так:

const result = Identity(concatStr)
  .ap(Identity("Hello"))
  .ap(Identity(" world !"));

Найдите время, чтобы перечитать, если вы чувствуете себя пораженным.

Последняя версия RESSEMBLE для регулярного вызова функции, чем предыдущий. Мы кормием Concatstr С его аргументами в лево-правильном порядке:

И все это происходит внутри нашего защитного контейнера.

Наконец, мы можем дополнительно очистить этот процесс параметризации.

Функция под названием Лифт2 сделай это:

// liftA2 :: Apply functor F => (a -> b -> c) -> F a -> F b -> F c
const liftA2 = curry((fn, F1, F2) => F1.map(fn).ap(F2));

// ...

const result = liftA2(concatStr, Identity("Hello"), Identity(" world !"));

Я уверен, что мы можем договориться, что это имя действительно неловко.

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

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

Однако эта метафора просто частично верна, потому что аргументы уже даны в их контейнере.

Интересная часть – это тело функции.

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

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

const liftA2 = curry((fn, F1, F2) => F(fn).ap(F1).ap(F2));
//                                   ↑ what's F ? We need the precise constructor.

Итак, используя левую версию, мы абстрактным типом функтора бесплатно.

Теперь вы можете подумать: «Хорошо, но что, если функция требует 3, 4 или более аргументов?»

Если это так, вы можете строить варианты, просто расширяя наши предыдущие Лифт2 :

// liftA3 :: Apply functor F => (a -> b -> c -> d) -> F a -> F b -> F c -> F d
const liftA3 = curry((fn, F1, F2, F3) => F1.map(fn).ap(F2).ap(F3));

// liftA4 :: Apply functor F => (a -> b -> c -> d -> e) -> F a -> F b -> F c -> F d -> F e
const liftA4 = curry((fn, F1, F2, F3, F4) => F1.map(fn).ap(F2).ap(F3).ap(F4));

// take3Args :: String -> String -> Number -> String
const take3Args = curry(
  (firstname, lastname, age) =>
    `My name is ${firstname} ${lastname} and I'm ${age}.`
);

// take4Args :: a -> b -> c -> d -> [a, b, c, d]
const take4Args = curry((a, b, c, d) => [a, b, c, d]);

liftA3(take3Args, Identity("Yann"), Identity("Salmon"), Identity(18));
// Identity("My name is Yann Salmon and I'm 18.")

liftA4(take4Args, Identity(1), Identity(2), Identity(3), Identity(4));
// Identity([1, 2, 3, 4])

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

Ух ты! Мы накрыли кучу вещей.

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

У нас почти есть полноценная панель инструментов для решения проблем реальных мировых проблем.

Теперь нам нужно исследовать Монад интерфейс.

Упражнения (набор 4)

Рассмотрим этот объект пользователя для следующих 2 упражнений:

const user = {
  id: "012345",
  name: "John Doe",
  hobbies: ["Cycling", "Drawing"],
  friends: [
    {name: "Mickael Bolp", ...},
    // ...
  ],
  partner: {name: "Theresa Doe", ...},
  // ...
}
  1. Создайте функцию, которая возвращает фразу, описывающую пару, если у пользователя есть партнер с использованием данных помощников и ап :
// safeProp :: String -> Object -> Maybe a
const safeProp = curry((prop, obj) =>
  obj[prop] === undefined || obj[prop] === null
    ? Maybe.Nothing()
    : Maybe.Just(obj[prop])
);

// getCouplePresentation :: User -> User -> String
const getCouplePresentation = curry(
  (name1, name2) => `${name1} and ${name2} are partners.`
);

// getName :: User -> String
const getName = (user) => user.name;
// I could have written: const getName = safeProp("name")
// but I didn't and that's intentional.
// We assume that a user always has a name.

const couple = ...

2. Refactor Предыдущий ответ Использование Лифт2 (Проверьте ответ предыдущего вопроса ранее):

// liftA2 :: Apply functor F => (a -> b -> c) -> F a -> F b -> F c
const liftA2 = curry((fn, F1, F2) => F1.map(fn).ap(F2));

const couple = ...

Проверить ответы .

Монад

В упражнениях незадолго я дал помощника считать тогда как мы могли бы получить это от SafeProp Отказ

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

Таким образом, пытаясь получить имя пользователя партнера пользователя, мы заканчиваем 2 вложенным Может быть Функторы:

const getPartnerName = pipe(safeProp("partner"), map(safeProp("name")));
// Maybe(Maybe("Theresa Doe"))

Давайте увидим еще один пример, где эта проблема становится еще хуже:

// getUser :: Object -> IO User
const getUser = ({ email, password }) => IO.of(db.getUser(email, password));

// getLastPurchases :: User -> IO [Purchase]
const getLastPurchases = (user) => IO.of(db.purchases(user));

// display :: [Purchase] -> IO _
const display = "some implementation";

// displayUserPurchases :: Object -> IO _
const displayUserPurchases = pipe(
  getUser,
  map(getLastPurchases),
  map(map(display))
);

displayUserPurchases({ email: "johndoe@whatever.com", password: "1234" });
// IO(IO(IO _))

Как избавиться от этих слоев контейнера, который принудил нас, чтобы сделать вложенные карта Что ухудшает читаемость?

Монады на наше спасение! Монады – это функторы, которые могут сгладить.

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

Однако они мощные абстракции, которые связывают определенный набор поведения со значением.

Это структуры данных, созданные математическими законами, которые делают их чрезвычайно предсказуемыми и надежными.

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

Помните, что мы видели с примерами и ап :

F(x).map(fn) === F(fn).ap(F(x));

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

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

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

Вот почему я не очень беспокоил вас с этими законами. Но будьте в курсе своей утилиты (что, безусловно, простирается за пределы этого).

Возвращаясь к нашим монадам, поведение уплощения обычно реализуется с цепь (aka flatmap , связывать , > == ) Метод:

const Identity = (x) => ({
  inspect: () => `Identity(${x})`,
  // Functor interface
  map: (fn) => Identity(fn(x)),
  // Applicative interface
  ap: (functor) => functor.map(x),
  // Monad interface
  chain: (fn) => fn(x),
  value: x,
});

// Pointed interface
Identity.of = (x) => Identity(x);

// chain :: Monad M => (a -> M b) -> M a -> M b
const chain = curry((fn, monad) => monad.chain(fn));

const getPartnerName = pipe(safeProp("partner"), chain(safeProp("name")));

В случае Личность , цепь это как карта Но без нового Личность Функтор окружает его.

Вы можете подумать: «Это побеждает цель, мы вернемся на значение Unboxed!»

Но мы не будем, потому что FN предназначен для возврата функтора.

Посмотрите на тип подписи этого цепь помощник:

// chain :: Monad M => (a -> M b) -> M a -> M b
const chain = curry((fn, monad) => monad.chain(fn));

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

Например:

const Identity = (x) => ({
  // ...
  chain: (fn) => Identity(x).map(fn).value,
  value: x,
});

Вы можете увидеть, что мы сначала завернули х Затем карта, затем возьмите внутреннее значение.

Потому что упаковка х в новом Личность И в конечном итоге выбирая внутреннее значение напротив, это уборщик, чтобы никто из таких в первой версии.

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

// BEFORE
// ...

// displayUserPurchases :: Object -> IO _
const displayUserPurchases = pipe(
  getUser,
  map(getLastPurchases),
  map(map(display))
);

displayUserPurchases({ email: "johndoe@whatever.com", password: "1234" });
// IO(IO(IO _))

// AFTER
// ...

const displayUserPurchases = pipe(
  getUser,
  chain(getLastPurchases),
  chain(display)
);

displayUserPurchases({ email: "johndoe@whatever.com", password: "1234" });
// IO _

Во-первых, GetUser Возвращает IO (пользователь) Отказ

Затем мы цепи getlastpurchases вместо того, чтобы отображать его.

Другими словами, мы сохраняем результат getlastpurchases (пользователь) (который является IO (?) ), избавиться от оригинала Ио что окружено Пользователь Отказ

Вот почему монады часто сравниваются с луком – уравновешивающие/цепи их подобно удалению слоя лука. Когда вы делаете это, вы выделяете потенциальные нежелательные результаты, которые могут заставить вас плакать?,

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

Этот функтор не работает.

Тем не менее, нам нужно расширить простую версию, которую мы увидели ранее в этом посте, чтобы дать ему Применитель и Монад интерфейсы.

В противном случае мы не могли использовать его как таковые:

const Nothing = () => ({
  inspect: () => `Nothing()`,
  map: Nothing,
  ap: Nothing,
  chain: Nothing,
});

Nothing.of = () => Nothing();

Пока вы храните хотя бы один слой (это один функтор), пока не готов выпустить эффект, это нормально.

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

Реконструировать

Функторы Примените функцию на обернутое значение ( Map ).

Замечал У функционирования есть метод для размещения значения в минимальном контексте функции функции функтора ( ).

Укладки Примените обернутую функцию в обернутое значение ( AP + ).

Монады Примените функцию, которая возвращает обернутое значение для завернутого значения ( цепочка + ).

Упражнения (набор 5)

  1. Рассмотрим этот объект:
const restaurant = {
  name: "The Creamery",
  address: {
    city: "Los Angeles",
    street: {
      name: "Melrose Avenue",
    },
  },
  rating: 8,
};

Создать функцию getstreetname Что, как и название, возвращает название улицы ресторана.

Использовать SafePropцепочка вместе с любыми другими функциональными помощниками, которые вам нужны) сделать это чистым образом.

// safeProp :: String -> Object -> Maybe a
const safeProp = curry((prop, obj) =>
  obj[prop] === undefined || obj[prop] === null
    ? Maybe.Nothing()
    : Maybe.Just(obj[prop])
);

// getStreetName :: Object -> Maybe String
const getStreetName = ...

Проверить ответы .

Упражнения ответы

Ответы, которые я предлагаю, не единственные. Вы можете придумать свои собственные, даже лучшие решения.

Пока ваше решение работает, это здорово.

Установить 1.

Вернуться к упражнению.

  1. Чистые функции: A, D, E/Удобные функции: B, C

Для е Ответ может быть нелегко понять.

Это была эта функция:

const counter = (start, end) => {
  // ...

  // e
  () => counter(start + 1, end);
};

Так что это одна функция внутри другой.

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

На чистом функциональном языке, в отличие от JavaScript, счетчик , Начать и конец будет неизменяться так е Будет чистым, потому что для того же ввода (в этом случае нет), мы всегда получим тот же выход.

Однако значения в JavaScript по умолчанию смеются.

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

В этом случае е будет считаться нечистым.

Но потому что здесь не так, я класс как чистая функция.

Смотрите это нить Больше подробностей.

2.

const people = [
  { firstname: "Bill", lastname: "Harold", age: 54 },
  { firstname: "Ana", lastname: "Atkins", age: 42 },
  { firstname: "John", lastname: "Doe", age: 57 },
  { firstname: "Davy", lastname: "Johnson", age: 34 },
];

const uppercaseNames = (person) => ({
  firstname: person.firstname.toUpperCase(),
  lastname: person.lastname.toUpperCase(),
  age: person.age,
});

// "sort" mutates the original array it's applied on.
// So I make a copy before ([...people]) to not mutate the original argument.
const sortByAge = (people) =>
  [...people].sort((person1, person2) => person1.age - person2.age);

const parsePeople = (people) => sortByAge(people.map(uppercaseNames));

// NOT SURE TO INCLUDE
// If you have already read the section on Composition (after this one), you may come up with
// a more readable version for "parsePeople":
const parsePeople = pipe(map(uppercaseNames), sortByAge);
// or
const parsePeople = compose(sortByAge, map(uppercaseNames));

parsePeople(people);
// [
//   {firstname: "DAVY", lastname: "JOHNSON", age: 34},
//   {firstname: "ANA", lastname: "ATKINS", age: 42},
//   {firstname: "BILL", lastname: "HAROLD", age: 54},
//   {firstname: "JOHN", lastname: "DOE", age: 57},
// ]

Это версия, с которой я пришел, но любая вариация работает с момента, когда у него нет побочных эффектов.

Функция в упражнении действительно мутирует объект, переданный в качестве аргумента.

Но вы можете проверить, что оригинал люди Массив неизменен в этой коррекции.

Набор 2

Вернуться к упражнению.

const input =
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

// ...

// keepLetters :: [Character] -> [Character] | []
const keepLetters = filter((char) =>
  "abcdefghijklmnopqrstuvwxyz".includes(char)
);

// getLetters :: String -> [Character]
const getLetters = pipe(
  lowercase,
  getChars,
  keepLetters,
  removeDuplicates,
  sort
);
// or
const getLetters = compose(
  sort,
  removeDuplicates,
  keepLetters,
  getChars,
  lowercase
);

getLetters(input);
// ["a", "b", "c", "d", "e", "f", "g", "h", "i", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "x"]

2.

// getMedianAges :: [Key, [Person]] ->  [Key, Object]
const getMedianAges = reduceOverVal((acc, person) => {
  const key = `medianAge${person.sex}`;

  return !acc[key]
    ? { ...acc, [key]: person.age }
    : { ...acc, [key]: mean(acc[key], person.age) };
}, {});

// groupsMedianAges :: Object -> Object
const groupsMedianAges = pipe(getEntries, map(getMedianAges), fromEntries);
// or
const groupsMedianAges = compose(fromEntries, map(getMedianAges), getEntries);

3.

// reduce :: (b -> a -> b) -> b -> [a] -> b

4.

// curry :: ((a, b, ...) -> c) -> a -> b ->  ... -> c

Набор 3

Вернуться к упражнению.

const uppercaseF = map((str) => str.toUpperCase())

// Example:
const myFunctor = Just("string")

uppercaseF(myFunctor)
// Just("STRING")

2.

const uppercaseF = map((str) => str.toUpperCase());

// Example:
const myFunctor = Just("string");

uppercaseF(myFunctor);
// Just("STRING")
```

2.

```js
// printUsername :: User -> _
const printUsername = pipe(
  safeProp("name"),
  uppercaseF,
  maybe("Username not found !", console.log)
);

// Example:
printUsername({
  name: "Yann Salmon",
  age: 18,
  interests: ["Programming", "Sport", "Reading", "Math"],
  // ...
});
// console: YANN SALMON

Набор 4

Вернуться к упражнению.

// getPartnerName :: User -> Maybe String
const getPartnerName = pipe(safeProp("partner"), map(getName));

// userName :: Maybe String
const userName = Maybe.of(getName(user));
// partnerName :: Maybe String
const partnerName = getPartnerName(user);

// couple :: Maybe String
const couple = Maybe.of(getCouplePresentation).ap(userName).ap(partnerName);
// Just("John Doe and Theresa Doe are partners.")

2.

// ...

const couple = liftA2(getCouplePresentation, userName, partnerName);

Установить 5.

Вернуться к упражнению.

// ...

// getStreetName :: Object -> Maybe String
const getStreetName = pipe(
  safeProp("address"),
  chain(safeProp("street")),
  chain(safeProp("name"))
);

getStreetName(restaurant);
// Just("Melrose Avenue")

Идти дальше

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

Как и я, вы, безусловно, найдуте несколько концепций действительно трудно понять сначала.

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

Я гарантирую вас, что он окупится.

Есть также отличный Github Repository которые собирают ресурсы о функциональном программировании в JavaScript.

Вы найдете, среди прочего, хорошие библиотеки, которые предоставляют функциональные помощники. Мой любимый в то время – Ramda JS Отказ Другие также предоставляют монады, как Святилище .. Отказ

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

Те, о которых я знаю:

  • Техника под названием трансвицинг Отказ Короче, это способ сочинения карта , Фильтр и Уменьшить Операции вместе. Проверьте это и что Узнать больше.
  • Другие распространенные виды монад: либо, карта, список
  • Другие алгебраические структуры, такие как полугруппы и моноиды
  • Функциональное реактивное программирование

Заключение

Вот и все!

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

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

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

И с этим, продолжайте кодирование! ?

Оригинал: “https://www.freecodecamp.org/news/the-principles-of-functional-programming/”