Сначала правила
Во-первых, у нас есть Закон естественной трансформации.
- Некоторый функтор
F
из a
, сопоставленный с функцией f
, дает F
из b
, затем естественным образом преобразуется, дает некоторый функтор G
из b
.
- Некоторый функтор
F
из a
, естественным образом преобразованный, дает некоторый функтор G
из a
, затем сопоставленный с некоторой функцией f
, дает G
из b
Выбор любого пути (сначала сопоставить, затем преобразовать, или сначала преобразовать, затем сопоставить) приведет к одному и тому же конечному результату, G
из b
.
nt(x.map(f)) == nt(x).map(f)
Получить реальность
Хорошо, теперь давайте сделаем практический пример. Я объясню код шаг за шагом, а затем в самом конце у меня будет полный работоспособный пример.
Сначала мы реализуем Либо (используя Left
и Right
)
const Left = x => ({
map: f => Left(x),
fold: (f,_) => f(x)
})
const Right = x => ({
map: f => Right(f(x)),
fold: (_,f) => f(x),
})
Затем мы реализуем Task
const Task = fork => ({
fork,
// "chain" could be called "bind" or "flatMap", name doesn't matter
chain: f =>
Task((reject, resolve) =>
fork(reject,
x => f(x).fork(reject, resolve)))
})
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))
Теперь давайте начнем определять некоторые части теоретической программы. У нас будет база данных пользователей, где у каждого пользователя есть лучший друг (лучший друг навсегда). Мы также определим простую функцию Db.find
, которая возвращает задачу поиска пользователя в нашей базе данных. Это похоже на любую библиотеку базы данных, которая возвращает Promise.
// fake database
const data = {
"1": {id: 1, name: 'bob', bff: 2},
"2": {id: 2, name: 'alice', bff: 1}
}
// fake db api
const Db = {
find: id =>
Task((reject, resolve) =>
resolve((id in data) ? Right(data[id]) : Left('not found')))
}
Итак, есть один небольшой поворот. Наша функция Db.find
возвращает Task
из Either
(Left
или Right
). Это в основном для демонстрационных целей, но также может рассматриваться как хорошая практика. То есть, мы можем не считать сценарий «пользователь не найден» ошибкой, поэтому мы не хотим reject
задачу — вместо этого мы изящно обработаем ее позже, разрешив Left
из 'not found'
. Мы могли бы использовать reject
в случае другой ошибки, такой как сбой подключения к базе данных или что-то в этом роде.
Постановка целей
Цель нашей программы — взять заданный идентификатор пользователя и найти его лучшую подругу.
Мы амбициозны, но наивны, поэтому сначала попробуем что-то вроде этого
const main = id =>
Db.find(1) // Task(Right(User))
.map(either => // Right(User)
either.map(user => // User
Db.find(user.bff))) // Right(Task(Right(user)))
Да! a Task(Right(Task(Right(User))))
... это очень быстро вышло из-под контроля. Это будет полный кошмар, работая с таким результатом...
Естественная трансформация
А вот и наше первое естественное преобразование eitherToTask
:
const eitherToTask = e =>
e.fold(Task.rejected, Task.of)
// eitherToTask(Left(x)) == Task.rejected(x)
// eitherToTask(Right(x)) == Task.of(x)
Давайте посмотрим, что произойдет, когда мы chain
преобразуем это преобразование в наш Db.find
результат.
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // ???
...
Так что же такое ???
? Что ж, Task#chain
ожидает, что ваша функция вернет Task
, а затем сплющит текущую задачу и только что возвращенную задачу вместе. Итак, в этом случае мы идем:
// Db.find // eitherToTask // chain
Task(Right(User)) -> Task(Task(User)) -> Task(User)
Ух ты. Это уже огромное улучшение, потому что оно делает наши данные более плоскими по мере прохождения вычислений. Давайте продолжим ...
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // Task(User)
.chain(user => Db.find(user.bff)) // ???
...
Так что же такое ???
на этом шаге? Мы знаем, что Db.find
возвращает Task(Right(User)
, но мы делаем chain
, так что мы знаем, что вместе сожмем как минимум два Task
. Это значит, что мы идем:
// Task of Db.find // chain
Task(Task(Right(User))) -> Task(Right(User))
И посмотрите, у нас есть еще один Task(Right(User))
, который мы уже умеем сглаживать. eitherToTask
!
const main = id =>
Db.find(id) // Task(Right(User))
.chain(eitherToTask) // Task(User)
.chain(user => Db.find(user.bff)) // Task(Right(User))
.chain(eitherToTask) // Task(User) !!!
Горячий картофель! Итак, как мы будем работать с этим? Итак, main
принимает Int
и возвращает Task(User)
, так что...
// main :: Int -> Task(User)
main(1).fork(console.error, console.log)
Это действительно так просто. Если Db.find
разрешает право, оно преобразуется в Task.of
(решенная задача), то есть результат переходит к console.log
, иначе, если Db.find
разрешает левое, оно преобразуется в Task.rejected
(отклоненная задача), что означает результат пойдет на console.error
Выполняемый код
// Either
const Left = x => ({
map: f => Left(x),
fold: (f,_) => f(x)
})
const Right = x => ({
map: f => Right(f(x)),
fold: (_,f) => f(x),
})
// Task
const Task = fork => ({
fork,
chain: f =>
Task((reject, resolve) =>
fork(reject,
x => f(x).fork(reject, resolve)))
})
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))
// natural transformation
const eitherToTask = e =>
e.fold(Task.rejected, Task.of)
// fake database
const data = {
"1": {id: 1, name: 'bob', bff: 2},
"2": {id: 2, name: 'alice', bff: 1}
}
// fake db api
const Db = {
find: id =>
Task((reject, resolve) =>
resolve((id in data) ? Right(data[id]) : Left('not found')))
}
// your program
const main = id =>
Db.find(id)
.chain(eitherToTask)
.chain(user => Db.find(user.bff))
.chain(eitherToTask)
// bob's bff
main(1).fork(console.error, console.log)
// alice's bff
main(2).fork(console.error, console.log)
// unknown user's bff
main(3).fork(console.error, console.log)
Атрибуция
Почти всем этим ответом я обязан Брайану Лонсдорфу (@drboolean). У него есть фантастическая серия статей об Egghead под названием Профессор Фрисби представляет компонуемый функциональный JavaScript. . Совершенно случайно пример в вашем вопросе (преобразование будущего и любого) — это тот же пример, который используется в его видео и в этом коде в моем ответе здесь.
Два о естественных преобразованиях
- Преобразование основных типов с естественными преобразованиями а>
- Применение естественных преобразований в повседневной работе а>
Альтернативная реализация задачи
В Task#chain
есть немного волшебства, которое не сразу бросается в глаза
task.chain(f) == task.map(f).join()
Я упоминаю об этом в качестве примечания, потому что это не особенно важно для рассмотрения естественного преобразования Либо в Задачу выше. Task#chain
достаточно для демонстрации, но если вы действительно хотите разобрать его, чтобы посмотреть, как все работает, он может показаться немного неприступным.
Ниже я вывожу chain
, используя map
и join
. Я помещу пару аннотаций типа ниже, которые должны помочь
const Task = fork => ({
fork,
// map :: Task a => (a -> b) -> Task b
map (f) {
return Task((reject, resolve) =>
fork(reject, x => resolve(f(x))))
},
// join :: Task (Task a) => () -> Task a
join () {
return Task((reject, resolve) =>
fork(reject,
task => task.fork(reject, resolve)))
},
// chain :: Task a => (a -> Task b) -> Task b
chain (f) {
return this.map(f).join()
}
})
// these stay the same
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))
Вы можете заменить определение старой задачи на это новое в приведенном выше примере, и все будет работать по-прежнему ^_^
Переход на родной язык с Promise
ES6 поставляется с промисами, которые могут функционировать очень похоже на реализованную нами задачу. Конечно, есть куча отличий, но для целей этой демонстрации использование Promise вместо Task приведет к коду, почти идентичному исходному примеру.
Основные отличия:
- Задача ожидает, что ваши параметры функции
fork
будут упорядочены как (reject, resolve)
- параметры функции исполнителя обещания упорядочены как (resolve, reject)
(обратный порядок)
- мы называем
promise.then
вместо task.chain
- Промисы автоматически сжимают вложенные промисы, поэтому вам не нужно беспокоиться о ручном выравнивании промисов промисов.
Promise.rejected
и Promise.resolve
нельзя назвать первоклассными — контекст каждого из них должен быть привязан к Promise
— например, x => Promise.resolve(x)
или Promise.resolve.bind(Promise)
вместо Promise.resolve
(то же самое для Promise.reject
)
// Either
const Left = x => ({
map: f => Left(x),
fold: (f,_) => f(x)
})
const Right = x => ({
map: f => Right(f(x)),
fold: (_,f) => f(x),
})
// natural transformation
const eitherToPromise = e =>
e.fold(x => Promise.reject(x),
x => Promise.resolve(x))
// fake database
const data = {
"1": {id: 1, name: 'bob', bff: 2},
"2": {id: 2, name: 'alice', bff: 1}
}
// fake db api
const Db = {
find: id =>
new Promise((resolve, reject) =>
resolve((id in data) ? Right(data[id]) : Left('not found')))
}
// your program
const main = id =>
Db.find(id)
.then(eitherToPromise)
.then(user => Db.find(user.bff))
.then(eitherToPromise)
// bob's bff
main(1).then(console.log, console.error)
// alice's bff
main(2).then(console.log, console.error)
// unknown user's bff
main(3).then(console.log, console.error)
person
Mulan
schedule
14.03.2017