IO monad предотвращает короткое замыкание встроенного mapM?

Несколько озадачен следующим кодом. В не-игрушечной версии проблемы я пытаюсь сделать монадическое вычисление в monad Result, значения которого могут быть построены только из IO. Похоже, что магия за IO делает такие вычисления строгими, но я не могу понять, как именно это происходит.

Код:

data Result a = Result a | Failure deriving (Show)

instance Functor Result where
  fmap f (Result a) = Result (f a)
  fmap f Failure = Failure

instance Applicative Result where
  pure = return
  (<*>) = ap

instance Monad Result where
  return = Result
  Result a >>= f = f a
  Failure >>= _ = Failure

compute :: Int -> Result Int
compute 3 = Failure
compute x = traceShow x $ Result x

compute2 :: Monad m => Int -> m (Result Int)
compute2 3 = return Failure
compute2 x = traceShow x $ return $ Result x

compute3 :: Monad m => Int -> m (Result Int)
compute3 = return . compute

main :: IO ()
main = do
  let results = mapM compute [1..5]
  print $ results
  results2 <- mapM compute2 [1..5]
  print $ sequence results2
  results3 <- mapM compute3 [1..5]
  print $ sequence results3
  let results2' = runIdentity $ mapM compute2 [1..5]
  print $ sequence results2'

Выход:

1
2
Failure
1
2
4
5
Failure
1
2
Failure
1
2
Failure

Ответ 1

Хорошие тестовые примеры. Вот что происходит:

  • в mapM compute мы, как обычно, видим лень на работе. Здесь нет ничего удивительного.

  • in mapM compute2 мы работаем внутри монады IO, чье определение mapM потребует весь список: в отличие от Result, который пропускает хвост списка, как только Failure найден, IO всегда будет проверять весь список. Обратите внимание на код:

    compute2 x = traceShow x $ return $ Result x
    

    Таким образом, вышеприведенное wil распечатает отладочное сообщение, как только будет доступен доступ к каждому элементу списка операций ввода-вывода. Все, поэтому мы печатаем все.

  • в mapM compute3 мы используем примерно:

    compute3 x = return $ traceShow x $ Result x
    

    Теперь, поскольку return в IO является ленивым, он не будет запускать traceShow при возврате действия ввода-вывода. Итак, когда mapM compute3 запущен, сообщения не видно. Вместо этого мы видим сообщения только тогда, когда выполняется sequence results3, что заставляет Result - не все из них, но только столько, сколько необходимо.

  • окончательный пример Identity также довольно сложный. Обратите внимание:

    > newtype Id1 a = Id1 a
    > data Id2 a = Id2 a
    > Id1 (trace "hey!" True) `seq` 42
    hey!
    42
    > Id2 (trace "hey!" True) `seq` 42
    42
    

    при использовании newtype во время выполнения не задействован бокс /unboxing (AKA lift), поэтому принудительное значение Id1 x вызывает принуждение x. При типах data этого не происходит: значение обернуто в поле (например, Id2 undefined не эквивалентно undefined).

    В вашем примере вы добавляете конструктор Identity, но это из newtype Identity!! Поэтому при вызове

    return $ traceShow x $ Result x
    

    return здесь ничего не переносит, а traceShow немедленно запускается, как только выполняется mapM.

Ответ 2

Тип Result кажется практически идентичным Maybe, с

Result <-> Just
Failure <-> Nothing

Для моего бедного мозга я останусь в терминологии Maybe в остальном ответе.

chi объяснил, почему IO (Maybe a) не замыкается так, как вы ожидали. Но есть тип, который вы можете использовать для такого рода вещей! По сути, это тот же самый тип, но с другим экземпляром Monad. Вы можете найти его в Control.Monad.Trans.Maybe. Это выглядит примерно так:

newtype MaybeT m a = MaybeT
  { runMaybeT :: m (Maybe a) }

Как вы можете видеть, это всего лишь обертка newtype вокруг m (Maybe a). Но его экземпляр Monad очень отличается:

instance Monad m => Monad (MaybeT m) where
  return a = MaybeT $ return (Just a)
  m >>= f = MaybeT $ do
    mres <- runMaybeT m
    case mres of
      Nothing -> return Nothing
      Just a -> runMaybeT (f a)

То есть m >>= f выполняет вычисление m в основной монаде, получая Maybe что-то или что-то другое. Если он получает Nothing, он просто останавливается, возвращая Nothing. Если он что-то получает, он передает это значение в f и запускает результат. Вы также можете превратить любое действие m в "успешное" действие MaybeT m, используя lift из Control.Monad.Trans.Class:

class MonadTrans t where
  lift :: Monad m => m a -> t m a

instance MonadTrans MaybeT where
  lift m = MaybeT $ Just <$> m

Вы также можете использовать этот класс, определенный где-то вроде Control.Monad.IO.Class, который часто бывает более четким и может быть намного удобнее:

class MonadIO m where
  liftIO :: IO a -> m a

instance MonadIO IO where
  liftIO m = m

instance MonadIO m => MonadIO (MaybeT m) where
  liftIO m = lift (liftIO m)