каждый знает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. Актуальный бой
Практические примеры последних трех занятий курса см.:настоящий бой.