web-dev-qa-db-ja.com

「ダミー」のための最も単純な非自明なモナド変換の例、IO + Maybe

誰かが非常に単純な(数行)モナド変換子の例を挙げてもらえますか?.

たとえば、誰かがIOを実行し、失敗を処理できるモナドをどのように作成しますか?

これを示す最も簡単な例は何ですか?

私はいくつかのモナドトランスフォーマーチュートリアルをざっと見てきましたが、それらはすべてState MonadやParsers、または何か複雑なもの(newbeeの場合)を使用しているようです。それよりももっとシンプルなものが見たいです。 IO +は簡単だと思いますが、自分でそれを行う方法がわかりません。

どうすればIO + Maybeモナドスタックを使用できますか?上に何がありますか?下には何がありますか?どうして?

どのような場合に、IO + MaybeモナドまたはMaybe + IOモナドを使用しますか?それはそのような複合モナドを作成することにはまったく意味がありますか?はいの場合、いつ、なぜですか?

40
jhegedus

これは here を.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_をmaybe + IOモナドスタックと考えることに慣れます。

_IO Int_がIntを返すモナド式であるのと同様に、_MaybeT IO Int_はIntを返す_MaybeT IO_式です。

複合型シグネチャの読み取りに慣れることは、モナド変換子を理解するための戦いの半分です。

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で実行できること以上のことはしていません。次に、Maybeモナドと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
_

さらに、2つの連続するliftIOステートメントは常に1つの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
_

これは(末尾の)再帰関数であり、正常に動作します。

従来の言語では、これをbreakステートメントを使用した無限のwhileループとして記述します。

_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_を書く方が簡単な場合があります。

84
ErikR

foo :: IO (Maybe a)func1 :: a -> IO (Maybe b)func2 :: b -> IO (Maybe c)など、ある意味で「失敗する可能性がある」IO値を処理する必要があるとします。

バインドのチェーンにエラーの存在を手動でチェックすると、恐ろしい「運命の階段」がすばやく生成されます。

_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
_

これを何らかの方法で「自動化」する方法は?おそらく、最初の引数がNothing内のIOかどうかを自動的にチェックするバインド関数を使用して、IO (Maybe a)の周りに新しいタイプを考案し、自分でチェックする手間を省くことができます。何かのようなもの

_newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }
_

バインド機能で:

_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を少し一般化すると、基礎となるすべてのモナドでこれを機能させることができます。

_newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }
_

そして、これは、本質的に、MaybeTトランスフォーマー 機能する です。トランスフォーマーにreturnを実装する方法、IO値を_MaybeOverM IO_値に「リフト」する方法など、いくつかの詳細は省略しました。

MaybeOverIOの種類は_* -> *_ですが、MaybeOverMの種類は_(* -> *) -> * -> *_です(最初の「型引数」はモナド型コンストラクタであるため、「型引数」が必要です。 ")。

13
danidiaz

確かに、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

その余分なモナド層を忘れて、通常のモナドとして扱うだけです。

7
AJFarmar