2016-05-31 9 views
11

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

Код:

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 

ответ

10

Nice Тестовые. Вот что происходит:

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

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

    compute2 x = traceShow x $ return $ Result x 
    

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

  • в 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, во время выполнения нет бокса/распаковки (AKA лифтинг) участвует, так принуждая значение Id1 x вызывает x быть вынужденным. С типами data этого не происходит: значение обернуто в поле (например, Id2 undefined не эквивалентно undefined).

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

    return $ traceShow x $ Result x 
    

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

+0

Большое спасибо за ваш ответ, chi. Могу ли я спросить, как вы знаете, что определение IO mapM является строгим и что возвращение является ленивым? – NioBium

+3

@NioBium 'Failure >> = f = Failure' отбрасывает' f': нет необходимости продолжать дальше в монадической цепочке. IO имеет определение нижнего рычага, которое нелегко записать, но - запрет на исключения - 'action >> = f' всегда будет вызывать' f', так как вы ожидаете, что, например, 'action >> print 4' в конечном итоге напечатает 4 независимо от того, что делает« действие »(запрет исключений IO и отказ от прерывания). – chi

+0

Справа. Еще раз спасибо! – NioBium

1

Ваш 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)