Монада IO предотвращает короткое замыкание встроенной карты M?

Несколько озадачен следующим кодом. В не-игрушечной версии задачи я пытаюсь выполнить монадическое вычисление в монаде 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

2 ответа

Решение

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

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

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

    compute2 x = traceShow x $ return $ Result x
    

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

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

    compute3 x = return $ traceShow x $ Result x
    

    Теперь, так как return в IO ленив, это не вызовет traceShow при возврате действия IO. Так когда 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, во время выполнения не включается бокс / распаковка (подъем АКА), поэтому принудительное Id1 x причины причин x быть вынужденным. С data Типы этого не происходит: значение обернуто в поле (например, Id2 undefined не эквивалентно undefined).

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

    return $ traceShow x $ Result x
    

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

Ваш Result тип, по-видимому, практически идентичен Maybe, с

Result <-> Just
Failure <-> Nothing

Ради моего бедного мозга я буду придерживаться Maybe Терминология в остальной части этого ответа.

чи объяснил почему 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)
Другие вопросы по тегам