5

我正在尝试使用 IO 和 Maybe monads 编写一个简单的示例。程序从 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

4

2 回答 2

3

您可以尝试编写一个EitherIO单子转换器。Monad 转换器允许您将两个 monad 的效果组合成一个 monad。它们可以用一种通用的方式编写,这样我们就可以根据需要创建 monad 的动态组合,但在这里我只演示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)

我们需要几个辅助函数来将其他平面类型带到我们的嵌套 monad

EitherIO.liftEither :: Either e a -> EitherIO e a
EitherIO.liftIO :: IO a -> EitherIO e a

为了符合幻想世界,我们的新EitherIO单子有一个chain方法和of功能,并遵守单子定律。map为了您的方便,我还用该方法实现了函子接口。

要么IO.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 的通用转换

monad 转换器将 monad 作为参数并创建一个新的 monad。在这种情况下,EitherT将采用一些 monadM并创建一个有效表现 has 的 monad M (Either e a)

所以现在我们有了一些方法来创建新的 monad

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

最后是一个自定义的 run 函数,它可以更轻松地处理我们的嵌套IO (Either e a)类型 - 注意,一层抽象 ( IO) 被删除了,所以我们只需要考虑Either

runEitherIO :: EitherIO e a -> Either e a

要么

是面包和黄油-您在这里看到的主要区别是EitherT接受 monadM作为输入并创建/返回新的 Monad 类型

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

要么IO

现在可以在以下方面实施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

于 2017-04-07T03:46:57.987 回答
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();
于 2017-04-06T20:41:02.737 回答