Глубокое обучение функциональному программированию серии (1) - частичный и карри

JavaScript

Что такое функциональное программирование

Перед статьей расскажу о функциональном программировании (Functional Programming, aka. FP) (ниже я буду использовать FP для обозначения функционального программирования):

  1. FP должен гарантировать, что функции являются чистыми, не полагаясь на внешние переменные состояния и не вызывая побочных эффектов. Исходя из этой посылки, комбинация и вызов чистых функций не будут иметь зависимостей во временном порядке, и не нужно беспокоиться о проблемах при изменении порядка вызова нескольких функций, поэтому также будут устранены многие потенциальные ошибки.
  2. Функции должны иметь вход и выход. Если функции не хватает ввода или вывода, это на самом деле обработчикprocedureВот и все.
  3. Функция максимально функционирует, чтобы поддерживать одну функцию, если функция делает более одной вещи, она должна быть теоретически разделена на несколько функций.
  4. Одно из значений FP заключается в том, что декларативное программирование используется в нужный момент для абстрагирования контроля и производительности потока программы, что лучше, чем императивное программирование с точки зрения понимания и обслуживания.
  5. ФП — это парадигма, но это не значит, что она конфликтует с ООП (объектно-ориентированным программированием), конечно, они могут гармонично сосуществовать. по моему мнениюReactНа самом деле, это очень хороший каштан~
  6. Первоклассные функции Javascript и характеристики замыканий определяют, что Javascript действительно является подходящей стадией для FP.

Понимание замыканий

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

Closure is when a function remembers and accesses variables from outside of its own scope, even when that function is executed in a different scope.

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

Простой пример выглядит следующим образом

// Closure demo
function cube(x) {
  let z = 1;
  return function larger(y) {
    return x * y * z++;
  };
}

const makeCube = cube(10);
console.log(makeCube(5)); // 50
console.log(makeCube(5)); // 100

Задумывались ли вы когда-нибудь о том, как функция makeCube или более крупная функция запоминает переменные x и z, не принадлежащие ее собственной области видимости? Посмотреть в консолиmakeCube.prototype, нажмите на собрание и обнаружите, что есть[[Scopes]]в этом встроенном домеClosure(cube)Запомните переменные x и z, когда функция больше вернется. Если вы вложите несколько слоев функций, вы также найдете несколько Closure(name) в[[Scopes]]изScopes[]В массиве найдите переменные по порядку.

Посмотрите на тестовый код ниже:

function cube(x) {
  return function wrapper(y) {
    let z = 1;
    return function larger() {
      return x * y * z++;
    };
  }
}

const makeCubeY = cube(10);
const makeCube = makeCubeY(5);
const $__VAR1__ = '1. This var is just for test.';
let $__VAR2__ = '2. This var is just for test.';
var $__VAR3__ = '3. This var is just for test.';
console.log(makeCubeY.prototype, makeCube.prototype);
console.log(makeCube()); // 50
console.log(makeCube()); // 100

РаспечататьmakeCubeY.prototype:

РаспечататьmakeCube.prototype:

С помощью этих экспериментов мы можем понять замыкания в Javascript с другой точки зрения: как замыкание находит переменные, которые не входят в его собственную область видимости? makeCube соответственно из[[Scopes]]Переменные y и z находятся в замыкании (оболочке) в , а переменная x находится в замыкании (кубе). Что касается переменных, объявленных глобальными параметрами let и const, то они размещаютсяScript, переменные, объявленные глобальной переменной var, помещаются вGlobalвнутри.

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

Вспомогательная функция

unary

const unary = fn => arg => fn(arg);

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

console.log(['1', '2', '3'].map(parseInt)); // [1, NaN, NaN]
console.log(['1', '2', '3'].map(unary(parseInt))); // [1, 2, 3]

parseInt(string, radix)Получает два параметра и функцию обратного вызова, полученную в функции картыcallback(currentValue[, index[, array]]), вторым параметром является индекс, если использование parseInt в данный момент является неправильным. Кроме конечноArray.prototype.map, функции обратного вызова в большом количестве встроенных методов массива не передают более одного параметра.Если есть применимый сценарий, требующий только первого параметра, унарная функция будет играть его значение, и это будет элегантно и лаконично подключен без изменения функции. (Для унарных функций fn — это переменные данные замыкающей памяти)

identity

const identity = v => v;

Некоторые студенты найдут функцию тождества необъяснимой? Для чего это? Я был сбит с толку с первого взгляда? Но рассмотрим следующий сценарий:

console.log([false, 1, 2, 0, '5', true].filter( identity )); // [1, 2, "5", true]
console.log([false, 0].some( identity )); // false
console.log([-2, 1, '3'].every( identity )); // true

как насчет этого? Ярко, не ожидалidentityИзначально функция была скрытой, но на самом деле, несмотря на то, что идентификатор возвращал исходное значение, в этих функциях Javascript преобразует тип возвращаемого значения в логическое значение. Например, функция фильтра. Мы видим, что MDN определяет описание фильтра следующим образом (см. предложение, выделенное жирным шрифтом).

filter() calls a provided callback function once for each element in an array, and constructs a new array of all the values for which callback returns a value that coerces to true.

constant

const constant = v => () => v;

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

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Hello!');
  }, 200);
});
p1.then(() => 'Hi').then(console.log); // Hi!
p1.then(constant('Hi')).then(console.log); // Hi!
p1.then('Hi').then(console.log); // Hello!

из-заPromise.prototype.thenПринимает только функции, если мне нужно только передать значение, тоconstantобеспечит это удобство. Конечно, это не функциональное улучшение, но оно улучшает читабельность, что также является преимуществом функционального программирования.

spreadArgs & gatherArgs

const spreadArgs = fn => argsArr => fn( ...argsArr );
const gatherArgs = fn => (...argsArr) => fn( argsArr );

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

Пример функции spreadArgs выглядит следующим образом:

function cube(x, y, z) {
  return x * y * z;
}

function make(fn, points) {
  return fn(points);
}

console.log(make(cube, [3, 4, 5])); // NaN
console.log(make(spreadArgs(cube), [3, 4, 5])); // 60

Пример функции collectArgs выглядит следующим образом:

function combineFirstTwo([v1, v2]) {
  return v1 + v2;
}

console.log([1, 2, 3, 4, 5].reduce(combineFirstTwo)); // Uncaught TypeError
console.log([1, 2, 3, 4, 5].reduce(gatherArgs(combineFirstTwo))); // 15

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

partial & curry

const partial = (fn, ...presetArgs) => (...laterArgs) =>
  fn(...presetArgs, ...laterArgs);
  
const curry = (fn, arity = fn.length, nextCurried) =>
  (nextCurried = prevArgs => nextArg => {
    const args = [...prevArgs, nextArg];

    if (args.length >= arity) {
      return fn(...args);
    } else {
      return nextCurried(args);
    }
  })([]);

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

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

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

выпей кофе~

Сначала смоделируйте функцию ajax следующим образом:

function ajax(url, params, callback) {
  setTimeout(() => {
    callback(
      `GET ${url} \nparams: ${params} \ndata: Hello! ${params} `
    );
  });
}

Рассмотрим сценарий частичного использования следующим образом:

const fetchPerson = partial( ajax, "http://some.api/person" );

fetchPerson('Teddy Bear', console.log);
/*
GET http://some.api/person 
params: Teddy Bear 
data: Hello! Teddy Bear 
*/

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

const fetchPerson = curry(ajax)('http://some.api/person');
const fetchUncleBarney = fetchPerson('Uncle Barney');

fetchUncleBarney(console.log);
/*
GET http://some.api/person 
params: Uncle Barney 
data: Hello! Uncle Barney 
*/

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

но! Я полагаю, что у студентов, прочитавших пример, будет ряд вопросительных знаков? Почему лучше не передавать параметры сразу, вместо того, чтобы передавать их несколько раз по отдельности? Причины следующие:

  1. Первая и основная причина заключается в том, что как частичные, так и карри-функции позволяют нам разделить время и пространство вызова функции посредством управления параметрами. Традиционные функции должны собирать параметры за один раз для вызова, но иногда мы можем заранее установить некоторые параметры и передать оставшиеся параметры только тогда, когда функция должна быть запущена окончательно. Вот где частичное и карри становятся очень полезными.
  2. Наличие партиала и карри позволяет функциям составлять (compose) было бы удобнее. (Комбинацией функций также планируется поделиться с вами позже, поэтому я не буду здесь вдаваться в подробности).
  3. Конечно, самое главное — улучшить читаемость! Сначала вы можете так не думать, но если вы будете практиковать этот опыт, он может измениться.

P.S. Что касается практики функционального программирования, вы можете использоватьlodash/fpмодуль для вводной практики.

некоторые размышления

Поскольку я тоже новичок в функциональном программировании, если что-то не так, поправьте меня~

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

Спасибо (●´∀`●)~