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)