Подтвердить что ты не робот

Простейший нетривиальный пример трансформатора монады для "манекенов", IO + Возможно

Может ли кто-нибудь дать супер простой (несколько строк) пример трансформатора монады, который является нетривиальным (т.е. не использует монадию Identity - что я понимаю).

Например, как бы кто-то создал монаду, которая делает IO, и может обрабатывать сбой (возможно)?

Каким будет самый простой пример, который продемонстрировал бы это?

Я просмотрел несколько учебников по трансформаторам монады, и все они, похоже, используют State Monad или Parsers или что-то сложное (для новичков). Я бы хотел увидеть что-то более простое. Я думаю, что IO + Возможно, будет просто, но я действительно не знаю, как это сделать.

Как я могу использовать стек mono IO + Maybe? Что будет наверху? Что будет внизу? Почему?

В каком варианте использования вы захотите использовать монаду IO + Maybe или монаду Maybe + IO? Было бы разумно создать такую ​​композиционную монаду? Если да, когда и почему?

4b9b3361

Ответ 1

Это доступно здесь как файл .lhs.

Трансформатор MaybeT позволит нам вырваться из вычисления монады, подобно тому, как бросать исключение.

Сначала я быстро перейду к некоторым предварительным комментариям. Для обработанного примера перейдите к Добавление возможно полномочий к IO.

Сначала импортируются:

 import Control.Monad
 import Control.Monad.Trans
 import Control.Monad.Trans.Maybe

Правила большого пальца:

В монодатном стеке IO всегда находится внизу.

Другие IO-подобные монады также, как правило, всегда появляются внизу, например. монада трансформатора состояния ST.

MaybeT m - это новый тип монады, который добавляет силу монады Maybe к монаде m - например. MaybeT IO.

Мы войдем в то, что эта власть позже. На данный момент привык думать о MaybeT IO как о возможном + монофоническом стеке.

Так же, как IO Int - выражение монады, возвращающее Int, MaybeT IO Int - выражение MaybeT IO, возвращающее Int.

Привыкание к чтению сигнатур составного типа - это половина битвы за понимание трансформаторов монады.

Каждое выражение в блоке do должно быть из одной монады.

т.е. это работает, потому что каждое утверждение находится в IO-монаде:

 greet :: IO ()                               -- type:
 greet = do putStr "What is your name? "      -- IO ()
            n <- getLine                      -- IO String
            putStrLn $ "Hello, " ++ n         -- IO ()

Это не сработает, потому что putStr не находится в монаде MaybeT IO:

mgreet :: MaybeT IO ()
mgreet = do putStr "What is your name? "    -- IO monad - need MaybeT IO here
            ...

К счастью, есть способ исправить это.

Чтобы преобразовать выражение IO в выражение MaybeT IO, используйте liftIO.

liftIO является полиморфным, но в нашем случае он имеет тип:

liftIO :: IO a -> MaybeT IO a

 mgreet :: MaybeT IO ()                             -- types:
 mgreet = do liftIO $ putStr "What is your name? "  -- MaybeT IO ()
             n <- liftIO getLine                    -- MaybeT IO String
             liftIO $ putStrLn $ "Hello, " ++ n     -- MaybeT IO ()

Теперь все утверждения в mgreet взяты из монады MaybeT IO.

Каждый монадный преобразователь имеет функцию "run".

Функция запуска "запускает" самый верхний уровень возврата стека монады значение из внутреннего слоя.

Для MaybeT IO функция запуска:

runMaybeT :: MaybeT IO a -> IO (Maybe a)

Пример:

ghci> :t runMaybeT mgreet 
mgreet :: IO (Maybe ())

ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()

Также попробуйте запустить:

runMaybeT (forever mgreet)

Вам нужно будет использовать Ctrl-C для выхода из цикла.

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

Добавление возможно полномочий в IO

Начнем с программы, которая задает несколько вопросов:

 askfor :: String -> IO String
 askfor prompt = do
   putStr $ "What is your " ++ prompt ++ "? "
   getLine

 survey :: IO (String,String)
 survey = do n <- askfor "name"
             c <- askfor "favorite color"
             return (n,c)

Теперь предположим, что мы хотим дать пользователю возможность завершить опрос рано, набрав END в ответ на вопрос. Мы можем справиться с этим следующим образом:

 askfor1 :: String -> IO (Maybe String)
 askfor1 prompt = do
   putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
   r <- getLine
   if r == "END"
     then return Nothing
     else return (Just r)

 survey1 :: IO (Maybe (String, String))
 survey1 = do
   ma <- askfor1 "name"
   case ma of
     Nothing -> return Nothing
     Just n  -> do mc <- askfor1 "favorite color"
                   case mc of
                     Nothing -> return Nothing
                     Just c  -> return (Just (n,c))

Проблема заключается в том, что survey1 имеет знакомый вопрос о лестнице, который не масштабируется, если мы добавим больше вопросов.

Мы можем использовать трансформатор монады MaybeT, чтобы помочь нам здесь.

 askfor2 :: String -> MaybeT IO String
 askfor2 prompt = do
   liftIO $ putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
   r <- liftIO getLine
   if r == "END"
     then MaybeT (return Nothing)    -- has type: MaybeT IO String
     else MaybeT (return (Just r))   -- has type: MaybeT IO String

Обратите внимание, что все статуты в askfor2 имеют один и тот же тип монады.

Мы использовали новую функцию:

MaybeT :: IO (Maybe a) -> MaybeT IO a

Вот как выглядят типы:

                  Nothing     :: Maybe String
           return Nothing     :: IO (Maybe String)
   MaybeT (return Nothing)    :: MaybeT IO String

                 Just "foo"   :: Maybe String
         return (Just "foo")  :: IO (Maybe String)
 MaybeT (return (Just "foo")) :: MaybeT IO String

Здесь return - из IO-монады.

Теперь мы можем написать нашу функцию опроса следующим образом:

 survey2 :: IO (Maybe (String,String))
 survey2 =
   runMaybeT $ do a <- askfor2 "name"
                  b <- askfor2 "favorite color"
                  return (a,b)

Попробуйте запустить survey2 и рано закончите вопросы, набрав END в ответ на любой вопрос.

Короткие разрезы

Я знаю, что получаю комментарии от людей, если не упомянуть о следующих сокращениях.

Выражение:

MaybeT (return (Just r))    -- return is from the IO monad

также может быть записана просто как:

return r                    -- return is from the MaybeT IO monad

Кроме того, другим способом записи MaybeT (return Nothing) является:

mzero

Кроме того, два последовательных оператора liftIO могут всегда объединяться в один liftIO, например:

do liftIO $ statement1
   liftIO $ statement2 

совпадает с:

liftIO $ do statement1
            statement2

С этими изменениями наша функция askfor2 может быть записана:

askfor2 prompt = do
  r <- liftIO $ do
         putStr $ "What is your " ++ prompt ++ " (type END to quit)?"
         getLine
  if r == "END"
    then mzero      -- break out of the monad
    else return r   -- continue, returning r

В некотором смысле, mzero становится способом выхода из монады - как бросания исключения.

Другой пример

Рассмотрим этот простой цикл запроса пароля:

loop1 = do putStr "Password:"
           p <- getLine
           if p == "SECRET"
             then return ()
             else loop1

Это рекурсивная функция (хвост) и работает нормально.

В обычном языке мы можем написать это как бесконечный цикл while с инструкцией break:

def loop():
    while True:
        p = raw_prompt("Password: ")
        if p == "SECRET":
            break

С MaybeT мы можем написать цикл таким же образом, как и код Python:

loop2 :: IO (Maybe ())
loop2 = runMaybeT $
          forever $
            do liftIO $ putStr "Password: "
               p <- liftIO $ getLine
               if p == "SECRET"
                 then mzero           -- break out of the loop
                 else return ()

Последний return () продолжает выполнение, и поскольку мы находимся в цикле forever, управление переходит к началу блока do. Обратите внимание, что единственным значением, возвращаемым loop2, является Nothing, что соответствует выходу из цикла.

В зависимости от ситуации вам может легче написать loop2 вместо рекурсивного loop1.

Ответ 2

Предположим, вам нужно работать с значениями IO, которые могут "сбой" в некотором смысле, например foo :: IO (Maybe a), func1 :: a -> IO (Maybe b) и func2 :: b -> IO (Maybe c).

В ручном режиме проверка наличия ошибок в цепочке привязок быстро создает ужасную "лестницу гибели":

do
    ma <- foo
    case ma of
        Nothing -> return Nothing
        Just a -> do
            mb <- func1 a
            case mb of
                Nothing -> return Nothing
                Just b -> func2 b

Как "автоматизировать" это в некотором роде? Возможно, мы могли бы создать новый тип вокруг IO (Maybe a) с функцией связывания, которая автоматически проверяет, является ли первый аргумент Nothing внутри IO, что избавляет нас от необходимости проверять его сами. Что-то вроде

newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }

С функцией bind:

betterBind :: MaybeOverIO a -> (a -> MaybeOverIO b) -> MaybeOverIO b
betterBind mia mf = MaybeOverIO $ do
       ma <- runMaybeOverIO mia
       case ma of
           Nothing -> return Nothing
           Just a  -> runMaybeOverIO (mf a)

Это работает! И, глядя на это более близко, мы понимаем, что мы не используем какие-либо особые функции, кроме моноды IO. Обобщая новый тип, мы могли бы сделать эту работу для любой основной монады!

newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }

И это, по сути, как работает MaybeT transformer . Я не упомянул несколько деталей, например, как реализовать return для трансформатора и как "поднять" значения IO в значения MaybeOverM IO.

Обратите внимание, что MaybeOverIO имеет вид * -> *, а MaybeOverM имеет вид (* -> *) -> * -> * (потому что его первый аргумент типа) является конструктором типа monad, который сам требует "аргумента типа" ).

Ответ 3

Конечно, монадный трансформатор MaybeT:

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

Мы можем реализовать свой экземпляр монады следующим образом:

instance (Monad m) => Monad (MaybeT m) where
    return a = MaybeT (return (Just a))

    (MaybeT mmv) >>= f = MaybeT $ do
        mv <- mmv
        case mv of
            Nothing -> return Nothing
            Just a  -> unMaybeT (f a)

Это позволит нам выполнить IO с возможностью изящества в определенных обстоятельствах.

Например, представьте, что у нас есть такая функция:

getDatabaseResult :: String -> IO (Maybe String)

Мы можем самостоятельно манипулировать монадами с результатом этой функции, но если мы составим ее так:

MaybeT . getDatabaseResult :: String -> MaybeT IO String

Мы можем забыть об этом дополнительном монадическом слое и просто относиться к нему как к нормальной монаде.