Комбинация Maybe и IO монад для чтения / записи DOM
Я пытаюсь подготовить простой пример, используя IO и Возможно монады. Программа читает узел из DOM и записывает некоторые innerHTML
к этому.
Я одержим комбинацией IO и Maybe, например, IO (Maybe NodeList)
,
Как я закорачиваю или выкидываю ошибку с этой настройкой?
Я мог бы использовать getOrElse
извлечь значение или установить значение по умолчанию, но установка значения по умолчанию на пустой массив ничего не помогает.
import R from 'ramda';
import { IO, Maybe } from 'ramda-fantasy';
const Just = Maybe.Just;
const Nothing = Maybe.Nothing;
// $ :: String -> Maybe NodeList
const $ = (selector) => {
const res = document.querySelectorAll(selector);
return res.length ? Just(res) : Nothing();
}
// getOrElse :: Monad m => m a -> a -> m a
var getOrElse = R.curry(function(val, m) {
return m.getOrElse(val);
});
// read :: String -> IO (Maybe NodeList)
const read = selector =>
IO(() => $(selector));
// write :: String -> DOMNode -> IO
const write = text =>
(domNode) =>
IO(() => domNode.innerHTML = text);
const prog = read('#app')
// What goes here? How do I short circuit or error?
.map(R.head)
.chain(write('Hello world'));
prog.runIO();
2 ответа
Вы можете попробовать написать EitherIO
монадный трансформатор. Трансформаторы монад позволяют объединять эффекты двух монад в одну монаду. Они могут быть написаны в общем виде, так что мы можем создавать динамические комбинации монад по мере необходимости, но здесь я просто собираюсь продемонстрировать статическую связь Either
а также IO
,
Сначала нам нужен путь из IO (Either e a)
в EitherIO e a
и путь из EitherIO e a
в IO (Either e a)
EitherIO :: IO (Either e a) -> EitherIO e a
runEitherIO :: EitherIO e a -> IO (Either e a)
И нам понадобится пара вспомогательных функций для переноса других плоских типов в нашу вложенную монаду.
EitherIO.liftEither :: Either e a -> EitherIO e a
EitherIO.liftIO :: IO a -> EitherIO e a
Чтобы соответствовать фантазии земли, наш новый EitherIO
монада имеет chain
метод и of
функционировать и подчиняется законам монады. Для вашего удобства я также реализовал интерфейс функтора с map
метод.
EitherIO.js
import { IO, Either } from 'ramda-fantasy'
const { Left, Right, either } = Either
// type EitherIO e a = IO (Either e a)
export const EitherIO = runEitherIO => ({
// runEitherIO :: IO (Either e a)
runEitherIO,
// map :: EitherIO e a => (a -> b) -> EitherIO e b
map: f =>
EitherIO(runEitherIO.map(m => m.map(f))),
// chain :: EitherIO e a => (a -> EitherIO e b) -> EitherIO e b
chain: f =>
EitherIO(runEitherIO.chain(
either (x => IO.of(Left(x)), (x => f(x).runEitherIO))))
})
// of :: a -> EitherIO e a
EitherIO.of = x => EitherIO(IO.of(Right.of(x)))
// liftEither :: Either e a -> EitherIO e a
export const liftEither = m => EitherIO(IO.of(m))
// liftIO :: IO a -> EitherIO e a
export const liftIO = m => EitherIO(m.map(Right))
// runEitherIO :: EitherIO e a -> IO (Either e a)
export const runEitherIO = m => m.runEitherIO
Адаптация вашей программы для использования EitherIO
Что хорошего в этом, это ваша read
а также write
функции хороши, как они есть - ничего в вашей программе не нужно менять, за исключением того, как мы структурируем вызовы в prog
import { compose } from 'ramda'
import { IO, Either } from 'ramda-fantasy'
const { Left, Right, either } = Either
import { EitherIO, liftEither, liftIO } from './EitherIO'
// ...
// prog :: IO (Either Error String)
const prog =
EitherIO(read('#app'))
.chain(compose(liftIO, write('Hello world')))
.runEitherIO
either (throwError, console.log) (prog.runIO())
Дополнительное объяснение
// prog :: IO (Either Error String)
const prog =
// read already returns IO (Either String DomNode)
// so we can plug it directly into EitherIO to work with our new type
EitherIO(read('#app'))
// write only returns IO (), so we have to use liftIO to return the correct EitherIO type that .chain is expecting
.chain(compose(liftIO, write('Hello world')))
// we don't care that EitherIO was used to do the hard work
// unwrap the EitherIO and just return (IO Either)
.runEitherIO
// this actually runs the program and clearly shows the fork
// if prog.runIO() causes an error, it will throw
// otherwise it will output any IO to the console
either (throwError, console.log) (prog.runIO())
Проверка на ошибки
Идти вперед и изменить '#app'
к некоторому несоответствующему селектору (например) '#foo'
, Перезапустите программу, и вы увидите соответствующую ошибку в консоли
Error: Could not find DOMNode
Runnable демо
Вы сделали это так далеко. В качестве вашей награды приведена демонстрационная версия: https://www.webpackbin.com/bins/-Kh5NqerKrROGRiRkkoA
Общее преобразование с использованием EitherT
Преобразователь монад принимает монаду в качестве аргумента и создает новую монаду. В этом случае, EitherT
займет какую-то монаду M
и создать монаду, которая эффективно ведет себя M (Either e a)
,
Так что теперь у нас есть способ создавать новые монады
// EitherIO :: IO (Either e a) -> EitherIO e a
const EitherIO = EitherT (IO)
И снова у нас есть функции для подъема плоских типов в наш вложенный тип
EitherIO.liftEither :: Either e a -> EitherIO e a
EitherIO.liftIO :: IO a -> EitherIO e a
Наконец, пользовательская функция запуска, которая облегчает работу с нашими вложенными IO (Either e a)
тип - уведомление, один слой абстракции (IO
) удаляется, поэтому мы должны думать только о Either
runEitherIO :: EitherIO e a -> Either e a
EitherT
это хлеб с маслом - основное отличие вы видите здесь в том, что EitherT
принимает монаду M
в качестве входных данных и создает / возвращает новый тип монады
// EitherT.js
import { Either } from 'ramda-fantasy'
const { Left, Right, either } = Either
export const EitherT = M => {
const Monad = runEitherT => ({
runEitherT,
chain: f =>
Monad(runEitherT.chain(either (x => M.of(Left(x)),
x => f(x).runEitherT)))
})
Monad.of = x => Monad(M.of(Right(x)))
return Monad
}
export const runEitherT = m => m.runEitherT
EitherIO
теперь может быть реализовано с точки зрения EitherT
- значительно упрощенная реализация
import { IO, Either } from 'ramda-fantasy'
import { EitherT, runEitherT } from './EitherT'
export const EitherIO = EitherT (IO)
// liftEither :: Either e a -> EitherIO e a
export const liftEither = m => EitherIO(IO.of(m))
// liftIO :: IO a -> EitherIO e a
export const liftIO = m => EitherIO(m.map(Either.Right))
// runEitherIO :: EitherIO e a -> Either e a
export const runEitherIO = m => runEitherT(m).runIO()
Обновления нашей программы
import { EitherIO, liftEither, liftIO, runEitherIO } from './EitherIO'
// ...
// prog :: () -> Either Error String
const prog = () =>
runEitherIO(EitherIO(read('#app'))
.chain(R.compose(liftIO, write('Hello world'))))
either (throwError, console.log) (prog())
Запуск демо-версии с использованием EitherT
Вот исполняемый код с использованием EitherT: https://www.webpackbin.com/bins/-Kh8S2NZ8ufBStUSK1EU
Вы можете создать вспомогательную функцию, которая будет условно соединяться с другой производящей IO-функцией, если данный предикат возвращает true. Если он возвращает ложь, он выдаст IO ()
,
// (a → Boolean) → (a → IO ()) → a → IO ()
const ioWhen = curry((pred, ioFn, val) =>
pred(val) ? ioFn(val) : IO(() => void 0))
const $ = document.querySelector.bind(document)
const read = selector =>
IO(() => $(selector))
const write = text => domNode =>
IO(() => domNode.innerHTML = text)
const prog = read('#app').chain(
ioWhen(node => node != null, write('Hello world'))
)
prog.runIO();