Комбинация 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();

https://www.webpackbin.com/bins/-Kh2ghQd99-ljiPys8Bd

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();
Другие вопросы по тегам