【问题标题】:Combining Maybe and IO monads for DOM read/write结合 Maybe 和 IO monad 进行 DOM 读/写
【发布时间】:2017-09-01 18:33:15
【问题描述】:

我正在尝试编写一个简单的示例使用 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

【问题讨论】:

标签: javascript functional-programming monads ramda.js fantasyland


【解决方案1】:

如果给定的谓词返回 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();

【讨论】:

    【解决方案2】:

    您可以尝试编写一个EitherIO monad 转换器。 Monad 转换器允许您将两个 monad 的效果组合成一个 monad。它们可以用一种通用的方式编写,这样我们就可以根据需要创建 monad 的动态组合,但在这里我将演示 EitherIO 的静态耦合。

    首先,我们需要一种从IO (Either e a)EitherIO e a 的方法以及从EitherIO e aIO (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 monad 具有chain 方法和of 函数并遵守monad 法则。为方便起见,我还使用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 的通用转换

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

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

    // 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 接受 monad M 作为输入并创建/返回新的 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
    

    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

    【讨论】:

    • 感谢您的详尽解释!我会反复阅读,直到我理解为止。
    • 不客气。 Monad 变形金刚对我来说是一项新的研究,我花了一周的时间阅读才开始很好地掌握它们。我想我最初是让它们变得比必要的更复杂!我想写一个通用的转换器EitherT,它可以让你做类似const EitherIO = EitherT (IO)的事情。这让您创建 monad 的动态嵌套,而不是我在回答中写的静态耦合。如果我有时间,我会在这个周末尝试解决这个问题^_^
    • 我在自己的研究中发现了这个 github.com/mattbierner/akh-either 不确定它是否会有所帮助
    • akh 存储库非常有用且全面,但经过精心设计。您必须浏览多个文件才能了解它们是如何组合在一起的,但它们都写得很好。
    • 我改编了一些在 akh 的 EitherT 转换器中找到的代码,并在此答案的更新中为您实现了一个简化版本。 Monad 转换器很有趣!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-22
    • 1970-01-01
    相关资源
    最近更新 更多