在不更改结果的情况下添加操作以重构 do-notation

Add action without changing result to refactor do-notation

我想在 Haskell 中顺序组合两个 monad 动作,丢弃第二个产生的任何值,并将参数传递给两个动作。目前我正在使用这样的 do-block:

ask = do
  result <- getLine
  putStrLn result
  return result

我希望写得更自由、更整洁一点,所以我尝试了这个:

ask' = getLine <* putStrLn

然而,这甚至没有类型检查,问题是 <* does not transfer the result of the first action to the second. I want to chain the actions like >>= does, but not change the result. The type should be (a -> m b) -> (a -> m c) -> (a -> m b), but Hoogle 没有产生合适的结果。实现这个功能组合的运算符是什么?

作为一种趋势,如果您在两个不同的地方使用同一个值,可能 最好在清晰的 do 块中为其命名,而不是而不是按毫无意义的风格。

cartesian monoidal categories, known to Haskellers as arrows 捕获了将信息流拆分为不同操作的抽象概念。就您而言,您基本上是在 IO Kleisli 类别中工作:

import Prelude hiding (id)
import Control.Arrow

ask' :: Kleisli IO () String
ask' = Kleisli (\()->getLine) >>> (putStrLn &&& id) >>> arr snd

我不认为写这样的代码是个好主意。

I want to sequentially compose two monad actions in Haskell, discarding any value produced by the second, and passing the argument to both actions.

这听起来像 Reader——函数类型 r -> m aReaderT r m a 同构,monad 通过隐式插入相同的 r 值来工作进入所有 "holes." 例如:

import Control.Applicative
import Control.Monad.Reader

example :: IO String
example = getLine >>= discarding putStrLn

discarding :: Monad m => (a -> m b) -> a -> m a
discarding action = runReaderT (ReaderT action *> ask)

你想要的运算符是这样的:

action `thingy` extra = action >>= discarding extra

当然 discarding 有一个更简单的实现:

discarding :: Applicative f => (a -> f b) -> a -> f a
discarding action a = action a *> return a

...所以最后我认为这真的是代码高尔夫。但在一个更复杂的程序中,这是一个更大规模的常见模式,它可能值得一试。基本上,如果你有:

a0 :: r -> m a0
a1 :: r -> m a1
   .
   .
   .
an :: r -> m an

那么就是:

ReaderT a0 :: ReaderT r m a0
ReaderT a1 :: ReaderT r m a1
   .
   .
   .
ReaderT an :: ReaderT r m an

然后:

runReaderT (ReaderT a0 <* ReaderT a1 <* ... <* ReaderT an) :: r -> m a0

为了完整起见,在这种特殊情况下 (IO) monad,您也可以为此目的滥用 bracket

bracket getLine putStrLn return

但我强烈反对,因为这比原来的 do-notation 块可读性差得多,它太丑了。

如前所述,在这种特殊情况下,为结果命名似乎是最好的方法。

另见 Should do-notation be avoided in Haskell?