Глубокое изучение функционального программирования JavaScript

JavaScript
Глубокое изучение функционального программирования JavaScript

каждый знаетJavaScriptможно использовать как面向对象или函数式Используемый язык программирования, понятный всем函数式编程не более чем включить副作用,函数组合,柯里化Эти концепции на самом деле не так. Если вы узнаете о них больше, вы обнаружите, что函数式编程Он также включает в себя множество дополнительных функций, таких какfunctor,monadЖдать. Веб-сайт курса за границейeggheadБыл профессор (по имени Фрисби), основанный наJavaScriptобъяснил函数式编程Очень хорошо, в основном представленоbox,semigroup,monoid,functor,applicative functor,monad,isomorphismДополнительные темы, такие как функциональное программирование. Весь курс состоит примерно из 30 разделов.Эта статья в основном является переводом и кратким изложением курса.Если у вас есть силы, настоятельно рекомендуется посмотреть оригинальный курс.Professor Frisby Introduces Composable Functional JavaScript. В конце курса есть небольшой практический проект, где вы можете потренировать свои руки и испытать этот другой метод программирования.Вот заявление заранее, курсы, представленные в этом курсеmonadЖдать高级特性Не каждый может использовать его в проекте, но он может расширить знания, а также помочь в обучении.haskellчистое функциональное программирование

1. Используйте контейнер (Box) для создания линейного потока данных

Нормальная функция такая:

function nextCharForNumberString (str) {
  const trimmed = str.trim();
  const number = parseInt(trimmed);
  const nextNumber = number + 1;
  return String.fromCharCode(nextNumber);
}

const result = nextCharForNumberString(' 64');
console.log(result); // "A"

С помощью Array этого можно добиться следующим образом:

const nextCharForNumberString = str =>
    [str]
    .map(s => s.trim())
    .map(s => parseInt(s))
    .map(i => i + 1)
    .map(i => String.fromCharCode(i));

const result = nextCharForNumberString(' 64');
console.log(result); // ["A"]

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

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});

const nextCharForNumberString = str =>
    Box(str)
    .map(s => s.trim())
    .map(s => parseInt(s))
    .map(i => i + 1)
    .map(i => String.fromCharCode(i));

const result = nextCharForNumberString(' 64');
console.log(String(result)); // "Box(A)"

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

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});

const f0 = x => x * 100; // think fo as a data
const add1 = f => x => f(x) + 1; // think add1 as a function
const add2 = f => x => f(x) + 2; // think add2 as a function
const g = Box(f0)
.map(f => add1(f))
.map(f => add2(f))
.fold(f => f);

const res = g(1);
console.log(res); // 103

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

2. ИспользуйтеBoxРефакторинг императивного кода

используется здесьBoxТо же, что и в предыдущем разделе:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});

императивmoneyToFloat:

const moneyToFloat = str =>
    parseFloat(str.replace(/\$/g, ''));

BoxРежимmoneyToFloat:

const moneyToFloat = str =>
    Box(str)
    .map(s => s.replace(/\$/g, ''))
    .fold(r => parseFloat(r));

мы используем здесьBoxрефакторингmoneyToFloat,BoxХорошо превращать вложенные выражения в одно за другим.map, здесь не очень сложно, но это хорошая практика.

императивpercentToFloat:

const percentToFloat = str => {
  const replaced = str.replace(/\%/g, '');
  const number = parseFloat(replaced);
  return number * 0.01;
};

BoxРежимpercentToFloat:

const percentToFloat = str =>
    Box(str)
    .map(str => str.replace(/\%/g, ''))
    .map(replaced => parseFloat(replaced))
    .fold(number => number * 0.01);

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

императивapplyDiscount:

const applyDiscount = (price, discount) => {
  const cost = moneyToFloat(price);
  const savings = percentToFloat(discount);
  return cost - cost * savings;
};

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

BoxРежимapplyDiscount:

const applyDiscount = (price, discount) =>
    Box(price)
    .map(price => moneyToFloat(price))
    .fold(cost =>
        Box(discount)
        .map(discount => percentToFloat(discount))
        .fold(savings => cost - cost * savings));

Теперь посмотрите на вывод этого набора кода:

const result = applyDiscount('$5.00', '20%');

console.log(String(result)); // "4"

если мы былиmoneyToFloatа такжеpercentToFloatРаспаковка не выполняется в (т.е.fold),ТакapplyDiscountНет необходимости упаковывать данные перед преобразованием (т.Box) вверх:

const moneyToFloat = str =>
    Box(str)
    .map(s => s.replace(/\$/g, ''))
    .map(r => parseFloat(r)); // here we don't fold the result out

const percentToFloat = str =>
    Box(str)
    .map(str => str.replace(/\%/g, ''))
    .map(replaced => parseFloat(replaced))
    .map(number => number * 0.01); // here we don't fold the result out

const applyDiscount = (price, discount) =>
    moneyToFloat(price)
    .fold(cost =>
        percentToFloat(discount)
        .fold(savings => cost - cost * savings));

const result = applyDiscount('$5.00', '20%');

console.log(String(result)); // "4"

3. ИспользуйтеEitherуправление ветвями

Eitherзначит либо нетRightто естьLeft. мы сначала осознаемRight:

const Right = x => ({
  map: f => Right(f(x)),
  toString: () => `Right(${x})`
});

const result = Right(3).map(x => x + 1).map(x => x / 2);
console.log(String(result)); // "Right(2)"

Здесь мы не реализуемRightизfold, но сначала реализоватьLeft:

const Left = x => ({
  map: f => Left(x),
  toString: () => `Left(${x})`
});

const result = Left(3).map(x => x + 1).map(x => x / 2);
console.log(String(result)); // "Left(3)"

Leftконтейнер сRightотличается, потому чтоLeftФункция преобразования входящих данных полностью игнорируется, сохраняя данные внутри контейнера как есть. имеютRightа такжеLeft, мы можем управлять потоком данных программы. Учитывая, что в программе часто бывают исключения, контейнер обычно неизвестного типаRightOrLeft.

Далее реализуемRightа такжеLeftконтейнерfoldметод, если неизвестный контейнерRight, затем используйте второй параметр функцииgЧтобы распаковать:

const Right = x => ({
  map: f => Right(f(x)),
  fold: (f, g) => g(x),
  toString: () => `Right(${x})`
});

Если неизвестный контейнерLeft, используется первый параметр функцииfЧтобы распаковать:

const Left = x => ({
  map: f => Left(x),
  fold: (f, g) => f(x),
  toString: () => `Left(${x})`
});

пройти тестRightа такжеLeftизfoldметод:

const result = Right(2).map(x => x + 1).map(x => x / 2).fold(x => 'error', x => x);
console.log(result); // 1.5
const result = Left(2).map(x => x + 1).map(x => x / 2).fold(x => 'error', x => x);
console.log(result); // 'error'

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

Давайте посмотрим пример:

const findColor = name =>
    ({red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'})[name];

const result = findColor('red').slice(1).toUpperCase();
console.log(result); // "FF4444"

Здесь, если мы зададим функциюfindColorвходящийgreen, будет сообщено об ошибке. Поэтому с помощьюEitherВыполнить обработку ошибок:

const findColor = name => {
  const found = {red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name];
  return found ? Right(found) : Left(null);
};

const result = findColor('green')
            .map(c => c.slice(1))
            .fold(e => 'no color',
                 c => c.toUpperCase());
console.log(result); // "no color"

Делая шаг вперед, мы можем выделитьnullобнаруженEitherконтейнер при упрощенииfindColorКод:

const fromNullable = x =>
    x != null ? Right(x) : Left(null); // [!=] will test both null and undefined

const findColor = name =>
    fromNullable({red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name]);

4. ИспользуйтеchainрешитьEitherвложенная проблема

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

const fs = require('fs');

const getPort = () => {
  try {
    const str = fs.readFileSync('config.json');
    const config = JSON.parse(str);
    return config.port;
  } catch (e) {
    return 3000;
  }
};

const result = getPort();
console.log(result); // 8888 or 3000

Мы используемEitherРефакторинг:

const fs = require('fs');

const tryCatch = f => {
  try {
    return Right(f());
  } catch (e) {
    return Left(e);
  }
};

const getPort = () =>
    tryCatch(() => fs.readFileSync('config.json'))
    .map(c => JSON.parse(c))
    .fold(
        e => 3000,
        obj => obj.port
    );

const result = getPort();
console.log(result); // 8888 or 3000

Это идеально после рефакторинга? мы использовалиJSON.parse,еслиconfig.jsonЕсли есть проблема с форматом файла, программа сообщит об ошибке:

SyntaxError: Unexpected end of JSON input

Поэтому необходимоJSONАнализируя отказ обработки исключений, мы можем продолжать использоватьtryCatchДля решения этой проблемы:

const getPort = () =>
    tryCatch(() => fs.readFileSync('config.json'))
    .map(c => tryCatch(() => JSON.parse(c)))
    .fold(
        left => 3000, // 第一个tryCatch失败
        right => right.fold( // 第一个tryCatch成功
            e => 3000, // JSON.parse失败
            c => c.port
        )
    );

Этот рефакторинг мы использовали дваждыtryCatch, в результате получается два слоя коробки, и, наконец, требуется две распаковки. Для того, чтобы решить эту проблему между коробками, мы можем датьRightа такжеLeftдобавить методchain:

const Right = x => ({
  chain: f => f(x),
  map: f => Right(f(x)),
  fold: (f, g) => g(x),
  toString: () => `Right(${x})`
});

const Left = x => ({
  chain: f => Left(x),
  map: f => Left(x),
  fold: (f, g) => f(x),
  toString: () => `Left(${x})`
});

когда мы используемmap, и не хотите добавлять еще один слой ящиков после преобразования данных, мы должны использоватьchain:

const getPort = () =>
    tryCatch(() => fs.readFileSync('config.json'))
    .chain(c => tryCatch(() => JSON.parse(c)))
    .fold(
        e => 3000,
        c => c.port
    );

5. Обязательное использование кодаEitherПример реализации

const openSite = () => {
  if (current_user) {
      return renderPage(current_user);
    }
    else {
      return showLogin();
    }
};

const openSite = () =>
    fromNullable(current_user)
    .fold(showLogin, renderPage);
const streetName = user => {
  const address = user.address;
  if (address) {
    const street = address.street;
    if (street) {
      return street.name;
    }
  }
  return 'no street';
};

const streetName = user =>
    fromNullable(user.address)
    .chain(a => fromNullable(a.street))
    .map(s => s.name)
    .fold(
        e => 'no street',
        n => n
    );
const concatUniq = (x, ys) => {
  const found = ys.filter(y => y ===x)[0];
  return found ? ys : ys.concat(x);
};

const cancatUniq = (x, ys) =>
    fromNullable(ys.filter(y => y ===x)[0])
    .fold(null => ys.concat(x), y => ys);
const wrapExamples = example => {
  if (example.previewPath) {
    try {
      example.preview = fs.readFileSync(example.previewPath);
    }
    catch (e) {}
  }
  return example;
};

const wrapExamples = example =>
    fromNullable(example.previewPath)
    .chain(path => tryCatch(() => fs.readFileSync(path)))
    .fold(
        () => example,
        preview => Object.assign({preview}, example)
    );

6. Полугруппы

Полугруппа – этоconcatвид метода иconcatМетод удовлетворяет закону ассоциативности. НапримерArrayа такжеString:

const res = "a".concat("b").concat("c");
const res = [1, 2].concat([3, 4].concat([5, 6])); // law of association

мы настраиваемSumполугруппа,SumТипы используются для суммирования:

const Sum = x => ({
  x,
  concat: o => Sum(x + o.x),
  toString: () => `Sum(${x})`
});

const res = Sum(1).concat(Sum(2));
console.log(String(res)); // "Sum(3)"

продолжать настраиватьAllполугруппа,AllТипы используются для каскадирования логических типов:

const All = x => ({
  x,
  concat: o => All(x && o.x),
  toString: () => `All(${x})`
});

const res = All(true).concat(All(false));
console.log(String(res)); // "All(false)"

продолжать определятьFirstполугруппа,Firstцепочка типовconcatМетод не меняет своего начального значения:

const First = x => ({
  x,
  concat: o => First(x),
  toString: () => `First(${x})`
});

const res = First('blah').concat(First('ice cream'));
console.log(String(res)); // "First(blah)"

7. Примеры полугрупп.

Сначала займите место здесь, а затем добавьте его позже.

const acct1 = Map({
  name: First('Nico'),
  isPaid: All(true),
  points: Sum(10),
  friends: ['Franklin']
});

const acct2 = Map({
  name: First('Nico'),
  isPaid: All(false),
  points: Sum(2),
  friends: ['Gatsby']
});

const res = acct1.concat(acct2);
console.log(res);

8. monoid

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

е・а = а・е = а

Мы будем полугруппойSumОбновление реализации до моноида требует только реализацииemptyвызовите модифицированный метод, чтобы получить унитарный элемент моноида:

const Sum = x => ({
  x,
  concat: o => Sum(x + o.x),
  toString: () => `Sum(${x})`
});

Sum.empty = () => Sum(0);

const res = Sum.empty().concat(Sum(1).concat(Sum(2)));
// const res = Sum(1).concat(Sum(2)).concat(Sum.empty());
console.log(String(res)); // "Sum(3)"

Затем мы продолжаемAllОбновление реализовано в виде моноида:

const All = x => ({
  x,
  concat: o => All(x && o.x),
  toString: () => `All(${x})`
});

All.empty = () => All(true);

const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"

Если мы попытаемся объединить полугруппуFirstТакже обновление до моноида обнаружит, что это невозможно, напримерFirst('hello').concat(…)Результат всегдаhello,ноFirst.empty().concat(First('hello'))результат не обязательноhello, поэтому мы не можем преобразовать полугруппуFirstОбновление до моноида. Это также показывает, что моноид должен быть полугруппой, но полугруппа не обязательно является моноидом. Полугруппа должна удовлетворять ассоциативности, а моноид не только должен удовлетворять ассоциативности, но также должен быть унитарный элемент.

9. Пример моноида

Сумма (сумма):

const Sum = x => ({
  x,
  concat: o => Sum(x + o.x),
  toString: () => `Sum(${x})`
});

Sum.empty = () => Sum(0);

Товар:

const Product = x => ({
  x,
  concat: o => Product(x * o.x),
  toString: () => `Product(${x})`
});

Product.empty = () => Product(1);

const res = Product.empty().concat(Product(2)).concat(Product(3));
console.log(String(res)); // "Product(6)"

Любой (при наличииtrueто есть вернутьtrue, иначе возвратfalse):

const Any = x => ({
  x,
  concat: o => Any(x || o.x),
  toString: () => `Any(${x})`
});

Any.empty = () => Any(false);

const res = Any.empty().concat(Any(false)).concat(Any(false));
console.log(String(res)); // "Any(false)"

Все (все естьtrueтолько чтобы вернутьсяtrue, иначе возвратfalse):

const All = x => ({
  x,
  concat: o => All(x && o.x),
  toString: () => `All(${x})`
});

All.empty = () => All(true);

const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"

Max (чтобы найти максимальное значение):

const Max = x => ({
  x,
  concat: o => Max(x > o.x ? x : o.x),
  toString: () => `Max(${x})`
});

Max.empty = () => Max(-Infinity);

const res = Max.empty().concat(Max(100)).concat(Max(200));
console.log(String(res)); // "Max(200)"

Min (чтобы найти минимальное значение):

const Min = x => ({
  x,
  concat: o => Min(x < o.x ? x : o.x),
  toString: () => `Min(${x})`
});

Min.empty = () => Min(Infinity);

const res = Min.empty().concat(Min(100)).concat(Min(200));
console.log(String(res)); // "Min(100)"

10. ИспользованиеfoldMapСуммировать коллекции

Предположим, нам нужноSumКоллекции агрегируются, что можно реализовать так:

const res = [Sum(1), Sum(2), Sum(3)]
	.reduce((acc, x) => acc.concat(x), Sum.empty());

console.log(res); // Sum(6)

Учитывая общность этой операции, ее можно выделить в функциюfold. использоватьnodeУстановитьimmutableа такжеimmutable-ext.immutable-extпри условииfoldметод:

const {Map, List} = require('immutable-ext');
const {Sum} = require('./monoid');

const res = List.of(Sum(1), Sum(2), Sum(3))
	.fold(Sum.empty());

console.log(res); // Sum(6)

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

Box(3).fold(x => x); // 3
Right(3).fold(e => e, x => x); // 3

Да, ноfoldСуть в распаковке. лицом к лицуBoxа такжеRightРаспаковка типа — это извлечение его значения; теперь распаковка коллекции — это извлечение агрегированного результата коллекции. Чтобы объединить несколько значений в коллекции в одно значение, вам нужно передать начальное значениеSum.empty(). поэтому, когда вы видитеfold, его следует рассматривать как значение типа, который может быть типом только с одним значением (например,Box,Right) и, возможно, коллекцию моноидов.

Перейдем к другой коллекцииMap:

const res = Map({brian: Sum(3), sara: Sum(5)})
	.fold(Sum.empty());

console.log(res); // Sum(8)

здесьMapЭто коллекция моноидов.Если это обычная коллекция данных, вы можете использовать коллекцию в первую очередь.mapМетод преобразует коллекцию в коллекцию моноидов:

const res = Map({brian: 3, sara: 5})
	.map(Sum)
	.fold(Sum.empty());

console.log(res); // Sum(8)
const res = List.of(1, 2, 3)
	.map(Sum)
	.fold(Sum.empty());

console.log(res); // Sum(6)

Мы можем вызвать это для набора обычных типов данныхmapПреобразуйте в коллекцию моноидного типа, а затем вызовитеfoldИзвлекается операция агрегации данных, т.е.foldMap:

const res = List.of(1, 2, 3)
	.foldMap(Sum, Sum.empty());

console.log(res); // Sum(6)

11. ИспользованиеLazyBoxленивая оценка

Первый взгляд назадBoxпример:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});

const res = Box(' 64')
            .map(s => s.trim())
            .map(s => parseInt(s))
            .map(i => i + 1)
            .map(i => String.fromCharCode(i))
            .fold(x => x.toLowerCase());

console.log(String(res)); // a

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

const LazyBox = g => ({
  map: f => LazyBox(() => f(g())),
  fold: f => f(g())
});

const res = LazyBox(() => ' 64')
			.map(s => s.trim())
            .map(s => parseInt(s))
            .map(i => i + 1)
            .map(i => String.fromCharCode(i))
            .fold(x => x.toLowerCase());

console.log(res); // a

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

12. ВTaskзахват побочных эффектов

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

const launchMissiles = () =>
	console.log('launch missiles!'); // 使用console.log模仿发射火箭

При использованииdata.taskВы можете использовать его ленивую функцию, чтобы отложить выполнение:

const Task = require('data.task');

const launchMissiles = () =>
	new Task((rej, res) => {
      console.log('launch missiles!');
      res('missile');
	});

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

const app = launchMissiles().map(x => x + '!');

app
.map(x => x + '!')
.fork(
	e => console.log('err', e),
  	x => console.log('success', x)
);

// launch missiles!
// success missile!!

передачаforkметод потянет триггер и выполнит ранее определенныйTaskи ряд функций преобразования данных, если они не называютсяfork,Taskсерединаconsole.logОперация не будет выполнена.

13. ИспользованиеTaskОбработка асинхронных задач

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

const fs = require('fs');

const app = () =>
	fs.readFile('config.json', 'utf-8', (err, contents) => {
      if (err) throw err;
      const  newContents = contents.replace(/8/g, '6');
      fs.writeFile('config1.json', newContents,
      	(err, success) => {
        if (err) throw err;
        console.log('success');
      })
	});

app();

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

const Task = require('data.task');
const fs = require('fs');

const readFile = (filename, enc) =>
	new Task((rej, res) =>
    	fs.readFile(filename, enc, (err, contents) =>
        	err ? rej(err) : res(contents)));

const writeFile = (filename, contents) =>
	new Task((rej, res) =>
    	fs.writeFile(filename, contents, (err, success) =>
        	err ? rej(err) : res(success)));

const app = () =>
	readFile('config.json', 'utf-8')
	.map(contents => contents.replace(/8/g, '6'))
	.chain(contents => writeFile('config1.json', contents));

app().fork(
	e => console.log(e),
  	x => console.log('success')
);

реализовано здесьappявляется чистой функцией, вызывающейapp().forkБудет выполнен ряд действий. посмотри сноваdata.taskПример чтения двух файлов по порядку на официальном сайте:

const fs = require('fs');
const Task = require('data.task');

const readFile = path =>
    new Task((rej, res) =>
        fs.readFile(path, 'utf-8', (error, contents) =>
            error ? rej(error) : res(contents)));

const concatenated = readFile('Task_test_file1.txt')
                    .chain(a =>
                        readFile('Task_test_file2.txt')
                        .map(b => a + b));

concatenated.fork(console.error, console.log);

14. Functor

Функтор - этоmapТип метода, и он должен соответствовать следующим двум условиям:

fx.map(f).map(g) == fx.map(x => g(f(x)))

fx.map(id) == id(fx), where const id = x => x

кBoxВведите в качестве примера:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res1 = Box('squirrels')
			.map(s => s.substr(5))
			.map(s => s.toUpperCase());
const res2 = Box('squirrels')
			.map(s => s.substr(5).toUpperCase());
console.log(res1, res2); // Box(RELS) Box(RELS)

очевидноBoxПервое условие выполнено. Обратите внимание здесьs = > s.substr(5).toUpperCase()На самом деле, сg(f(x))Это то же самое, мы полностью переопределяем его к следующему виду, не ведитесь на форму:

const f = s => s.substr(5);
const g = s => s.toUpperCase();
const h = s => g(f(s));

const res = Box('squirrels')
			.map(h);
console.log(res); // Box(RELS)

Далее проверяем, выполнено ли второе условие:

const id = x => x;
const res1 = Box('crayons').map(id);
const res2 = id(Box('crayons'));
console.log(res1, res2); // Box(crayons) Box(crayons)

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

15. Использованиеofметод для помещения значения в Pointed Functor

Остроконечный функторofфунктор метода,ofЭто можно понимать как заполнение функтора начальным значением. кBoxНапример:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});
Box.of = x => Box(x);

const res = Box.of(100);
console.log(res); // Box(100)

Вот еще один пример функтора, функтора ввода-вывода:

const R = require('ramda');

const IO = x => ({
  x, // here x is a function
  map: f => IO(R.compose(f, x)),
  fold: f => f(x) // get out x
});

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

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

const R = require('ramda');
const {IO} = require('./IO');

const fake_window = {
    innerWidth: '1000px',
    location: {
        href: "http://www.baidu.com/cpd/fe"
    }
};

const io_window = IO(() => fake_window);

const getWindowInnerWidth = io_window
.map(window => window.innerWidth)
.fold(x => x);

const split = x => s => s.split(x);

const getUrl = io_window
.map(R.prop('location'))
.map(R.prop('href'))
.map(split('/'))
.fold(x => x);

console.log(getWindowInnerWidth()); // 1000px
console.log(getUrl()); // [ 'http:', '', 'www.baidu.com', 'cpd', 'fe' ]

16. Monad

Функтор может применить функцию к обернутому (здесь «обернутый» означает, что значение существует в поле, то же самое ниже) значение:

Box(1).map(x => x + 1); // Box(2)

Аппликативный функтор может применить обернутую функцию к обернутому значению:

const add = x => x + 1;
Box(add).ap(Box(1)); // Box(2)

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

смотри сначалаBoxПример функтора:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.map(x => Box(x))
			.map(x => Box(x)); // Box(Box(Box(1)))
console.log(res); // Box([object Object])

Здесь мы постоянно звонимmapа такжеmapВозвращаемое значение переданной функции — это тип коробки. Очевидно, что это приведет к тому, что количество упаковочных слоев коробки будет продолжать накапливаться. Мы можем датьBoxУвеличиватьjoinметод распаковки:

const Box = x => ({
  map: f => Box(f(x)),
  join: () => x,
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.map(x => Box(x))
			.join()
			.map(x => Box(x))
			.join();
console.log(res); // Box(1)

определено здесьjoinПросто чтобы проиллюстрировать операцию распаковки, мы, конечно, можем использоватьfoldВыполните ту же функцию:

const Box = x => ({
  map: f => Box(f(x)),
  join: () => x,
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.map(x => Box(x))
			.fold(x => x)
			.map(x => Box(x))
			.fold(x => x);
console.log(res); // Box(1)

учитывая.map(...).join()общность, мы можем написатьBoxдобавить методchainВыполните эти два шага:

const Box = x => ({
  map: f => Box(f(x)),
  join: () => x,
  chain: f => Box(x).map(f).join(),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.chain(x => Box(x))
			.chain(x => Box(x));
console.log(res); // Box(1)

17. Карри

Это очень простые, прямые примеры, и если вы сможете понять эти примеры, вы поймете и каррирование:

const modulo = dvr => dvd => dvd % dvr;

const isOdd = modulo(2); // 求奇数

const filter = pred => xs => xs.filter(pred);

const getAllOdds = filter(isOdd);

const res1 = getAllOdds([1, 2, 3, 4]);
console.log(res1); // [1, 3]

const map = f => xs => xs.map(f);

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

const add1 = add(1);
const allAdd1 = map(add1);

const res2 = allAdd1([1, 2, 3]);
console.log(res2); // [2, 3, 4]

18. Applicative Functor

ранее представленныйBoxявляется функтором, для которого мы добавляемapметод, обновите его до аппликативного функтора:

const Box = x => ({
  ap: b2 => b2.map(x), // here x is a function
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(x => x + 1).ap(Box(2));
console.log(res); // Box(3)

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

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

const res = Box(add).ap(Box(2));
console.log(res); // Box([Function])

Очевидно, мы вызываем аппликативный функтор один разapОдин параметр можно исключить, здесьresТо, что хранится внутри, по-прежнему является функцией:y => 2 + y, но исключает параметрx. Мы можем непрерывно звонитьapметод:

const res = Box(add).ap(Box(2)).ap(Box(3));
console.log(res); // Box(5)

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

F(x).map(f) = F(f).ap(F(x))

то есть в сохраненном значенииxФунктор вызываетсяmap(f), что равносильно сохранению функцииfФунктор вызываетсяap(F(x)).

Затем мы осуществляем функцию утилиты, которая обрабатывает заявитель ФункторliftA2:

const liftA2 = (f, fx, fy) =>
	F(f).ap(fx).ap(fy);

Но здесь вам нужно знать конкретный тип функтораF, поэтому с помощью предыдущих тождеств мы продолжаем определять следующую общую формуliftA2:

const liftA2 = (f, fx, fy) =>
	fx.map(f).ap(fy);

попытайся:

const res1 = Box(add).ap(Box(2)).ap(Box(4));
const res2 = liftA2(add, Box(2), Box(4)); // utilize helper function liftA2

console.log(res1); // Box(6)
console.log(res2); // Box(6)

Конечно, мы также можем определить что-то вродеliftA3,liftA4и другие полезные функции:

const liftA3 = (f, fx, fy, fz) =>
	fx.map(f).ap(fy).ap(fz);

19. Примеры аппликативных функторов.

Сначала давайте определимeither:

const Right = x => ({
  ap: e2 => e2.map(x), // declare as a applicative, here x is a function
  chain: f => f(x), // declare as a monad
  map: f => Right(f(x)),
  fold: (f, g) => g(x),
  inspect: () => `Right(${x})`
});

const Left = x => ({
  ap: e2 => e2.map(x), // declare as a applicative, here x is a function
  chain: f => Left(x), // declare as a monad
  map: f => Left(x),
  fold: (f, g) => f(x),
  inspect: () => `Left(${x})`
});

const fromNullable = x =>
    x != null ? Right(x) : Left(null); // [!=] will test both null and undefined

const either = {
  	Right,
  	Left,
  	of: x => Right(x),
  	fromNullable
};

Как можно заметитьeitherИ монада, и аппликативный функтор.

Предположим, мы хотим вычислить страницу, кромеheaderа такжеfooterВысота снаружи:

const $ = selector =>
	either.of({selector, height: 10}); // fake DOM selector

const getScreenSize = (screen, header, footer) =>
	screen - (header.height + footer.height);

При использованииmonodизchainметод, который может быть реализован следующим образом:

const res = $('header')
	.chain(header =>
    	$('footer').map(footer =>
        	getScreenSize(800, header, footer)));
console.log(res); // Right(780)

также можно использоватьapplicativeРеализовано, но сначала требуется каррированиеgetScreenSize:

const getScreenSize = screen => header => footer =>
	screen - (header.height + footer.height);

const res1 = either.of(getScreenSize(800))
	.ap($('header'))
	.ap($('footer'));
const res2 = $('header')
	.map(getScreenSize(800))
	.ap($('footer'));
const res3 = liftA2(getScreenSize(800), $('header'), $('footer'));
console.log(res1, res2, res3); // Right(780) Right(780) Right(780)

20. Список аппликативных функторов

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

for (x in xs) {
  for (y in ys) {
    for (z in zs) {
      // your code here
    }
  }
}

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

const {List} = require('immutable-ext');

const merch = () =>
	List.of(x => y => z => `${x}-${y}-${z}`)
	.ap(List(['teeshirt', 'sweater']))
	.ap(List(['large', 'medium', 'small']))
	.ap(List(['black', 'white']));
const res = merch();
console.log(res);

21. Обработка параллельных асинхронных событий с помощью аппликативов

Предположим, мы хотим инициировать два запроса на чтение базы данных:

const Task = require('data.task');

const Db = ({
  find: id =>
  	new Task((rej, res) =>
    	setTimeOut(() => {
      		console.log(res);
        	res({id: id, title: `Project ${id}`}) 
    	}, 5000))
});

const report = (p1, p2) =>
	`Report: ${p1.title} compared to ${p2.title}`;

При использованииmonadизchainПоймите, тогда два асинхронных события могут выполняться только по порядку:

Db.find(20).chain(p1 =>
	Db.find(8).map(p2 =>
    	report(p1, p2)))
	.fork(console.error, console.log);

Рефакторинг с использованием аппликативов:

Task.of(p1 => p2 => report(p1, p2))
.ap(Db.find(20))
.ap(Db.find(8))
.fork(console.error, console.log);

22. [Task] => Task([])

Предположим, мы собираемся прочитать набор файлов:

const fs = require('fs');
const Task = require('data.task');
const futurize = require('futurize').futurize(Task);
const {List} = require('immutable-ext');

const readFile = futurize(fs.readFile);

const files = ['box.js', 'config.json'];
const res = files.map(fn => readFile(fn, 'utf-8'));
console.log(res);
// [ Task { fork: [Function], cleanup: [Function] },
//   Task { fork: [Function], cleanup: [Function] } ]

здесьresЯвляетсяTaskмассив, и мы хотимTask([])этот тип, какpromise.all()функция. мы можем использоватьtraverseспособ сделатьTaskТипы выпрыгивают из массива:

[Task] => Task([])

Реализация выглядит следующим образом:

const files = List(['box.js', 'config.json']);
files.traverse(Task.of, fn => readFile(fn, 'utf-8'))
  .fork(console.error, console.log);

23. {Task} => Task({})

Предположим, мы собираемся сделать набор http-запросов:

const fs = require('fs');
const Task = require('data.task');
const {List, Map} = require('immutable-ext');

const httpGet = (path, params) =>
	Task.of(`${path}: result`);

const res = Map({home: '/', about: '/about', blog: '/blod'})
.map(route => httpGet(route, {}));
console.log(res);
// Map { "home": Task, "about": Task, "blog": Task }

здесьresявляется значениемTaskизMap, в то время как мы хотимTask({})этот тип, какpromise.all()функция. мы можем использоватьtraverseспособ сделатьTaskтип изMapПрыжок внутрь наружу:

{Task} => Task({})

Реализация выглядит следующим образом:

Map({home: '/', about: '/about', blog: '/blod'})
.traverse(Task.of, route => httpGet(route, {}))
.fork(console.error, console.log);
// Map { "home": "/: result", "about": "/about: result", "blog": "/blod: result" }

24. Преобразование типов

В этом разделе описывается, как один тип функтора может быть преобразован в другой тип функтора. Например, будетeitherПеревести вTask:

const {Right, Left, fromNullable} = require('./either');
const Task = require('data.task');

const eitherToTask = e =>
	e.fold(Task.rejected, Task.of);

eitherToTask(Right('nightingale'))
.fork(
	e => console.error('err', e),
  	r => console.log('res', r)
); // res nightingale

eitherToTask(Left('nightingale'))
.fork(
	e => console.error('err', e),
  	r => console.log('res', r)
); // err nightingale

БудуBoxПеревести вEither:

const {Right, Left, fromNullable} = require('./either');
const Box = require('./box');

const boxToEither = b =>
	b.fold(Right);

const res = boxToEither(Box(100));
console.log(res); // Right(100)

Вы можете быть удивлены, почемуboxToEitherпреобразовать вRight, вместоLeft, причина в том, что преобразования типов, обсуждаемые в этом разделе, должны удовлетворять этому условию:

nt(fx).map(f) == nt(fx.map(f))

вntЯвляется аббревиатурой естественного преобразования, то есть преобразования естественного типа, все функции, удовлетворяющие этой формуле, являются преобразованием естественного типа. Затем обсудитеboxToEither, если ранее было преобразовано вLeft, давайте посмотрим, может ли формула все еще быть удовлетворена:

const boxToEither = b =>
	b.fold(Left);

const res1 = boxToEither(Box(100)).map(x => x * 2);
const res2 = boxToEither(Box(100).map(x => x * 2));
console.log(res1, res2); // Left(100) Left(200)

Очевидно, что вышеуказанные условия не выполняются.

Еще один взгляд на естественную функцию преобразования типовfirst:

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

const res1 = first([1, 2, 3]).map(x => x + 1);
const res2 = first([1, 2, 3].map(x => x + 1));
console.log(res1, res2); // Right(2) Right(2)

Предыдущая формула показывает, что дляfunctor, сначала выполните естественное преобразование типов, а затемmapэквивалентно первомуmapЗатем выполните естественное преобразование типов.

25. Пример преобразования типов

Первый взглядfirstВариант использования для:

const {fromNullable} = require('./either');

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

const largeNumbers = xs =>
	xs.filter(x => x > 100);

const res = first(largeNumbers([2, 400, 5, 1000]).map(x => x * 2));

console.log(res); // Right(800)

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

const res = first(largeNumbers([2, 400, 5, 1000])).map(x => x * 2);

console.log(res); // Right(800)

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

const {Right, Left} = require('./either');
const Task = require('data.task');

const fake = id => ({
  id,
  name: 'user1',
  best_friend_id: id + 1
}); // fake user infomation

const Db = ({
  find: id =>
  	new Task((rej, res) =>
    	res(id > 2 ? Right(fake(id)) : Left('not found')))
}); // fake database

const eitherToTask = e =>
	e.fold(Task.rejected, Task.of);

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

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

Db.find(3) // Task(Right(user))
.map(either =>
    either.map(user => Db.find(user.best_friend_id))) // Task(Either(Task(Either)))

если используется здесьchain, и посмотрите, как это работает:

Db.find(3) // Task(Right(user))
.chain(either =>
	either.map(user => Db.find(user.best_friend_id))) // Either(Task(Either))

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

Db.find(3) // Task(Right(user))
.map(eitherToTask) // Task(Task(user))

Чтобы удалить слой упаковки, мы вместо этого используемchain:

Db.find(3) // Task(Right(user))
.chain(eitherToTask) // Task(user)
.chain(user =>
	Db.find(user.best_friend_id)) // Task(Right(user))
.chain(eitherToTask)
.fork(
	console.error,
  	console.log
); // { id: 4, name: 'user1', best_friend_id: 5 }

26. изоморфизм

Обсуждаемый здесь изоморфизм — это не изоморфизм «внешнего и внутреннего изоморфизма», а пара функций, удовлетворяющих следующим требованиям:

from(to(x)) == x

to(from(y)) == y

Если можно найти пару функций, удовлетворяющих указанным выше требованиям, то тип данныхxс другим типом данныхyта же самая информация или структура, в этот момент мы говорим тип данныхxи тип данныхyизоморфен. НапримерStringа также[char]изоморфен:

const Iso = (to, from) =>({
  to,
  from
});

// String ~ [char]
const chars = Iso(s => s.split(''), arr => arr.join(''));

const res1 = chars.from(chars.to('hello world'));
const res2 = chars.to(chars.from(['a', 'b', 'c']));
console.log(res1, res2); // hello world [ 'a', 'b', 'c' ]

Какая от этого польза? Возьмем пример:

const filterString = (str1, str2, pred) =>
  chars.from(chars.to(str1 + str2).filter(pred));

const res1 = filterString('hello', 'HELLO', x => x.match(/[aeiou]/ig));

console.log(res1); // eoEO

const toUpperCase = (arr1, arr2) =>
  chars.to(chars.from(arr1.concat(arr2)).toUpperCase());

const res2 = toUpperCase(['h', 'e', 'l', 'l', 'o'], ['w', 'o', 'r', 'l', 'd']);

console.log(res2); // [ 'H', 'E', 'L', 'L', 'O', 'W', 'O', 'R', 'L', 'D' ]

Здесь мы используемArrayизfilterметод фильтрацииStringсимволы в;StringизtoUpperCaseметод для обработки преобразования регистра символьных массивов. Видно, что с помощью изоморфизма мы можем преобразовывать два разных типа данных и вызывать их методы.

27. Актуальный бой

Практические примеры последних трех занятий курса см.:настоящий бой.