Как "продолжить" в цикле "Monad"?

Часто мне приходилось пропускать оставшуюся часть итерации (например, continue в C) в Haskell:

forM_ [1..100] $ \ i ->
    a <- doSomeIO
    when (not $ isValid1 a) <skip_rest_of_the_iteration>
    b <- doSomeOtherIO a
    when (not $ isValid2 b) <skip_rest_of_the_iteration>
    ...

Однако мне не удалось найти простой способ сделать это. Единственный способ, о котором я знаю, вероятно, это Trans.Maybe, но нужно ли использовать преобразование монады для достижения чего-то такого тривиального?

Ответ 1

Помните, что подобные петли в Haskell не волшебны... они просто обычные первоклассные вещи, которые вы можете написать себе.

Для чего это стоит, я не думаю, что слишком полезно думать о MaybeT как трансформатор Monad. Для меня MaybeT - это просто оболочка newtype, чтобы дать альтернативную реализацию (>>=)... так же, как вы используете Product, Sum, First, And и т.д., Чтобы дать альтернативные реализации mappend и mempty.

Прямо сейчас (>>=) для вас - IO a -> (a -> IO b) -> IO b. Но было бы более полезно иметь (>>=) здесь IO (Maybe a) -> (a -> IO (Maybe b) -> IO (Maybe b). Как только вы перейдете к первому действию, которое возвращает Nothing, это действительно невозможно "привязать" дальше. Именно это дает вам MaybeT. Вы также получаете "пользовательский экземпляр" guard, guard :: Bool -> IO (Maybe a) вместо guard :: IO a.

forM_ [1..100] $ \i -> runMaybeT $ do
  a <- lift doSomeIO
  guard (isValid1 a)
  b <- lift $ doSomeOtherIO a
  guard (isValid2 b)
  ...

и что он:)

MaybeT тоже не волшебство, и вы можете добиться в основном того же эффекта, используя вложенный when s. Это не нужно, это просто делает вещи намного проще и чище:)

Ответ 2

Вот как вы это сделаете, используя рекурсию bare-bones:

loop [] = return ()   -- done with the loop
loop (x:xs) =
  do a <- doSomeIO
     if ...a...
        then return ()  -- exit the loop
        else do -- continuing with the loop
                b <- doSomeMoreIO
                if ...b...
                   then return () -- exit the loop
                   else do -- continuing with the loop
                           ...
                           loop xs -- perform the next iteration

а затем вызовите его с помощью:

loop [1..100]

Вы можете немного убрать это с помощью функции when из Control.Monad:

    loop [] = return ()
    loop (x:xs) =
      do a <- doSomeIO
         when (not ...a...) $ do
           b <- doSomeMoreIO
           when (not ...b...) $ do
             ...
             loop xs

В Control.Monad также есть unless.

Используя @Ørjan Johansen полезный совет, вот простой пример:

import Control.Monad

loop [] = return ()
loop (x:xs) = do
  putStrLn $ "x = " ++ show x
  a <- getLine
  when (a /= "stop") $ do
  b <- getLine
  when (b /= "stop") $ do
  print $ "iteration: " ++ show x ++ ": a = " ++ a ++ " b = " ++ b
  loop xs

main = loop [1..3]

Ответ 3

Если вы хотите перебрать список или другой контейнер для выполнения действий и/или создать итоговое значение, и вы найдете обычные удобные инструменты, такие как for_ и foldM, недостаточно хороши для работы, вы можете рассмотреть foldr, который достаточно силен для работы. Когда вы на самом деле не зацикливаете контейнер, вы можете использовать обычную старую рекурсию или потянуть что-то вроде https://hackage.haskell.org/package/loops или (для совершенно другого вкуса) https://hackage.haskell.org/package/machines или, возможно, https://hackage.haskell.org/package/pipes.