结合 Maybe 和 IO monad 进行 DOM 读/写

Wil*_*l M 5 javascript monads functional-programming ramda.js fantasyland

我正在尝试使用 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();
Run Code Online (Sandbox Code Playgroud)

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

Tha*_*you 3

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

\n\n

首先我们需要一条从 到 的方法IO (Either e a)EitherIO e a一条从 到 的EitherIO e a方法IO (Either e a)

\n\n
EitherIO :: IO (Either e a) -> EitherIO e a\nrunEitherIO :: EitherIO e a -> IO (Either e a)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们需要一些辅助函数来将其他平面类型放入我们的嵌套 monad 中

\n\n
EitherIO.liftEither :: Either e a -> EitherIO e a\nEitherIO.liftIO :: IO a -> EitherIO e a\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

要么IO.js

\n\n
import { IO, Either } from \'ramda-fantasy\'\nconst { Left, Right, either } = Either\n\n// type EitherIO e a = IO (Either e a)\nexport const EitherIO = runEitherIO => ({\n  // runEitherIO :: IO (Either e a)\n  runEitherIO, \n  // map :: EitherIO e a => (a -> b) -> EitherIO e b\n  map: f =>\n    EitherIO(runEitherIO.map(m => m.map(f))),\n  // chain :: EitherIO e a => (a -> EitherIO e b) -> EitherIO e b\n  chain: f =>\n    EitherIO(runEitherIO.chain(\n      either (x => IO.of(Left(x)), (x => f(x).runEitherIO))))\n})\n\n// of :: a -> EitherIO e a\nEitherIO.of = x => EitherIO(IO.of(Right.of(x)))\n\n// liftEither :: Either e a -> EitherIO e a\nexport const liftEither = m => EitherIO(IO.of(m))\n\n// liftIO :: IO a -> EitherIO e a\nexport const liftIO = m => EitherIO(m.map(Right))\n\n// runEitherIO :: EitherIO e a -> IO (Either e a)\nexport const runEitherIO = m => m.runEitherIO\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

调整您的程序以使用 EitherIO

\n\n

这样做的好处是你的函数read本身write就很好——除了我们如何构造调用之外,你的程序中没有任何东西需要改变prog

\n\n
import { compose } from \'ramda\'\nimport { IO, Either } from \'ramda-fantasy\'\nconst { Left, Right, either } = Either\nimport { EitherIO, liftEither, liftIO } from \'./EitherIO\'\n\n// ...\n\n// prog :: IO (Either Error String)\nconst prog =\n  EitherIO(read(\'#app\'))\n    .chain(compose(liftIO, write(\'Hello world\')))\n    .runEitherIO\n\neither (throwError, console.log) (prog.runIO())\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

附加说明

\n\n
// prog :: IO (Either Error String)\nconst prog =\n  // read already returns IO (Either String DomNode)\n  // so we can plug it directly into EitherIO to work with our new type\n  EitherIO(read(\'#app\'))\n    // write only returns IO (), so we have to use liftIO to return the correct EitherIO type that .chain is expecting\n    .chain(compose(liftIO, write(\'Hello world\')))\n    // we don\'t care that EitherIO was used to do the hard work\n    // unwrap the EitherIO and just return (IO Either)\n    .runEitherIO\n\n// this actually runs the program and clearly shows the fork\n// if prog.runIO() causes an error, it will throw\n// otherwise it will output any IO to the console\neither (throwError, console.log) (prog.runIO())\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

检查错误

\n\n

继续并更改\'#app\'为一些不匹配的选择器(例如)\'#foo\'。重新运行该程序,您将看到控制台中出现相应的错误

\n\n
Error: Could not find DOMNode\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

可运行的演示

\n\n

你已经做到了这一步。这是一个可运行的演示作为您的奖励:https://www.webpackbin.com/bins/-Kh5NqerKrROGRiRkkoA

\n\n
\n\n
\n\n

使用 EitherT 进行通用变换

\n\n

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

\n\n

所以现在我们有办法创建新的 monad

\n\n
// EitherIO :: IO (Either e a) -> EitherIO e a\nconst EitherIO = EitherT (IO)\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们再次拥有将平面类型提升为嵌套类型的函数

\n\n
EitherIO.liftEither :: Either e a -> EitherIO e a\nEitherIO.liftIO :: IO a -> EitherIO e a\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
runEitherIO :: EitherIO e a -> Either e a\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

要么T

\n\n

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

\n\n
// EitherT.js\nimport { Either } from \'ramda-fantasy\'\nconst { Left, Right, either } = Either\n\nexport const EitherT = M => {\n   const Monad = runEitherT => ({\n     runEitherT,\n     chain: f =>\n       Monad(runEitherT.chain(either (x => M.of(Left(x)),\n                                      x => f(x).runEitherT)))\n   })\n   Monad.of = x => Monad(M.of(Right(x)))\n   return Monad\n}\n\nexport const runEitherT = m => m.runEitherT\n
Run Code Online (Sandbox Code Playgroud)\n\n

要么IO

\n\n

现在可以用EitherT\xe2\x80\x93 来实现,这是一个大大简化的实现

\n\n
import { IO, Either } from \'ramda-fantasy\'\nimport { EitherT, runEitherT } from \'./EitherT\'\n\nexport const EitherIO = EitherT (IO)\n\n// liftEither :: Either e a -> EitherIO e a\nexport const liftEither = m => EitherIO(IO.of(m))\n\n// liftIO :: IO a -> EitherIO e a\nexport const liftIO = m => EitherIO(m.map(Either.Right))\n\n// runEitherIO :: EitherIO e a -> Either e a\nexport const runEitherIO = m => runEitherT(m).runIO()\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们的计划更新

\n\n
import { EitherIO, liftEither, liftIO, runEitherIO } from \'./EitherIO\'\n\n// ...\n\n// prog :: () -> Either Error String\nconst prog = () =>\n  runEitherIO(EitherIO(read(\'#app\'))\n    .chain(R.compose(liftIO, write(\'Hello world\'))))\n\neither (throwError, console.log) (prog())\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

使用 EitherT 运行演示

\n\n

这是使用 EitherT 的可运行代码:https://www.webpackbin.com/bins/-Kh8S2NZ8ufBStUSK1EU

\n