使用 IO 代码中的 Maybe 值重构“阶梯”

Refactoring “staircasing” with case of `Maybe` values in `IO` code

以下函数 f 尝试使用 IO (Maybe Int) 函数两次读取 Int 两次,但在成功读取一个 Int 后“短路”执行:

readInt :: IO (Maybe Int)

f :: IO (Maybe Int)
f = do
  n1 <- readInt
  case n1 of
      Just n' -> return (Just n')
      Nothing -> do
        n2 <- readInt
        case n2 of
              Just n' -> return (Just n')
              Nothing -> return Nothing

有什么好的方法可以重构这段代码吗?如果我将它扩展到三次尝试,这将变得非常毛茸茸......

(我的思考过程:看到这个“阶梯”告诉我也许我应该使用 MaybeMonad 实例,但因为它已经在 IO monad 中,然后我将不得不使用 MaybeT(?)。但是,我只需要 readInt 中的 one 就可以成功,所以 Maybe monad 的在第一个 Nothing 上出错的行为在这里是错误的...)

您可以使用 MaybeTMonadPlus 实例来使用 msum:

f :: MaybeT IO Int
f = msum [readInt, readInt, readInt]

首先,

n2 <- readInt
case n2 of
      Just n' -> return (Just n')
      Nothing -> return Nothing

实际上只是 readInt。您正在挑选一个 Maybe 值,以便将相同的值放在一起。

对于其余部分,我认为在这种情况下最简洁的方法是使用 maybe 函数。那么你就可以获得

f = maybe readInt (return . Just) =<< readInt

您需要 MaybeT 的替代实例:

instance (Functor m, Monad m) => Alternative (MaybeT m) where
    empty = mzero
    (<|>) = mplus

instance (Monad m) => MonadPlus (MaybeT m) where
    mzero = MaybeT (return Nothing)
    mplus x y = MaybeT $ do
        v <- runMaybeT x
        case v of
            Nothing -> runMaybeT y
            Just _  -> return v

即如果它是 Just,则计算第一个参数和 return 值,否则计算第二个参数和 return 值。

代码:

import Control.Applicative
import Control.Monad
import Control.Monad.Trans.Maybe
import Text.Read

readInt :: MaybeT IO Int
readInt = MaybeT $ readMaybe <$> getLine 

main = runMaybeT (readInt <|> readInt) >>= print

另一种方法是使用 free 包中的 iterative monad transformer

import Control.Monad.Trans.Iter (untilJust,retract,cutoff,IterT)

readInt :: IO (Maybe Int)
readInt = undefined

f' :: IterT IO Int
f' = untilJust readInt

f :: IO (Maybe Int)
f = (retract . cutoff 2) f'

其中cutoff指定最大重试次数。

这种方法的优点是,由于 IterTMonadPlus 实例,您可以轻松地 interleave other repeated actionsf'。例如记录操作或等待操作。