结合Maybe和IO Monad实现DOM读写

5
我正在尝试使用IO和Maybe单子来构建一个简单的例子。该程序从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


这里有一个使用Either而不是Maybe的版本:https://www.webpackbin.com/bins/-Kh36r7GT7TR1ksMcWMM也许这是正确的方法... - Will M
2个回答

3
您可以尝试编写一个EitherIO单子变换器。Monad变换器允许将两个单子的效果组合成一个单一的单子。它们可以以通用的方式编写,使我们可以根据需要创建动态组合的单子,但这里我只是演示了EitherIO的静态耦合。
首先,我们需要一种方法将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

为了符合fantasy land规范,我们的新的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

这个方法的好处是,您的readwrite函数可以保持原样 - 您的程序中除了如何在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

可运行的演示

您已经走到了这一步。这里有一个可运行的演示作为您的奖励: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是IT技术中的重要概念,其主要作用是接受一个单子(Monad)M作为输入,并创建/返回一个新的单子类型。

它是IT技术中的“面包和黄油”,是必不可少的。

// 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


谢谢您的详细解释!我会一遍又一遍地阅读,直到理解为止。 - Will M
非常感谢。Monad transformers 对我来说是一个新的学习领域,花了一周的时间阅读才开始逐渐掌握它们。我认为最初我把它们想得比必要的还要复杂!我想涵盖编写通用转换器 EitherT 的内容,这将让您做一些像 const EitherIO = EitherT (IO) 这样的事情。这使您可以创建动态嵌套的 monads,而不是我在答案中编写的静态耦合。如果我有时间,我会尝试在本周末完成它 ^_^ - Mulan
我在自己的研究中发现了这个 https://github.com/mattbierner/akh-either 不确定它是否有帮助 - Will M
akh存储库非常有用和全面,但相当高度工程化。您将不得不追踪几个文件以查看它们如何配合,但所有内容都写得非常好。 - Mulan
我改了一些在akh的EitherT转换器中找到的代码,并在这个答案的更新中为您实现了一个简化版本。Monad转换器非常有趣! - Mulan
显示剩余2条评论

2
您可以创建一个帮助函数,如果给定的谓词返回 true,则条件链式连接到另一个 IO 生成函数。如果它返回 false,则会产生一个 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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接