LT駆動の発表内容の下書きです。
下書きといっても十分にかいているつもりである。(読みやすくとはいっていない)
IO (Maybe String)
はモナドが入れ子になっている。IOなんかついてるととても扱いづらい。実際に体験してみたりしてみる。
IO String
は 実行するたびに文字列が返ってくる。返ってくる文字列の内容は実行するたびに変わるかもしれない。
Maybe String
は 評価すると文字列もしくは空っぽかもしれない値が返ってくる。評価するたびに結果は変わらない。
IO (Maybe String)
は、実行するたびに文字列もしくは空っぽの値が返ってくる。値は実行するたびに変わるかもしれない。
IO Maybe String
ではない。
(IO Maybe) String
と解釈されるため。これは型として不正。
IOは一つの型しか受け取らないが2つの型を受け取ろうとしているからである。
カインドを確認してみる。
ghci> :kind IO
IO :: * -> *
ghci> :kind Maybe
Maybe :: * -> *
ghci> :kind String
String :: *
ghci> :kind Maybe String
Maybe String :: *
Maybe String
が*
なのでIOに渡せるわけです。
というわけで、Maybe (IO String)
という型もつくれる。
実際の話をしよう。
環境変数を取得するlookupEnv
をつかってみます。
ghci> :type System.Environment.lookupEnv
System.Environment.lookupEnv :: String -> IO (Maybe String)
環境変数Hoge
を取得してみる
$ stack ghci
ghci> :type System.Environment.lookupEnv "Hoge"
System.Environment.lookupEnv "Hoge" :: IO (Maybe String)
ghci> System.Environment.lookupEnv "Hoge"
Nothing
lookupEnv
に引数を与えているので、型はIO (Maybe String)
になっています。
結果がNothing
なのはHoge
が未定義のためです。
$ Hoge="abc" stack ghci
ghci> System.Environment.lookupEnv "Hoge"
Just "abc"
$ Hoge="" stack ghci
ghci> System.Environment.lookupEnv "Hoge"
Just ""
さて、実際に、IO (Maybe String)
な値をつかっていくところをみていきます。
環境変数Hogeを愚直にdoをつかってかいてみます。
import System.Environment
import Data.Maybe
sample = do -- IOのためのdo
maybeHoge <- lookup "Hoge" -- maybeHoge は IOがとれて Maybe String という型になっている
return $ do -- Maybe のためのdo IOのdoの中にかけるように returnをつける
hoge <- maybeHoge -- hogeはMaybeがはずれて String
return . putStrLn $ hoge -- hogeの内容を出力。コンパイルが通るように returnをつけてあげる
細かい事を気にせずかいてみました。doが二重構造になっています。
sample関数の結果はつかってみます。
$ stack ghci
ghci> sample >>= fromMaybe (putStrLn "not found Hoge")
not found Hoge
$ Hoge=aaa stack ghci
ghci> sample >>= fromMaybe (putStrLn "not found Hoge")
aaa
sample
だけでは動作しません。
Hogeがなかったときの処理を追加して、Maybeを外してやることで動作します。
sample
の型はIO (Maybe (IO ()))
になっています。IO (Maybe ()
になってて欲しいところがこうなってるために
sampleをつくるときにHogeがなかったときの処理を追加しておくことでこの問題を回避してみます。
sample2 = do
maybeHoge <- lookupEnv "Hoge"
let hoge = fromMaybe "not found" maybeHoge -- この時点でMaybeを外しておく。Nothingなら "not found"になる
putStrLn hoge
$ stack ghci
ghci> sample2
not found
$ Hoge="aaa" stack ghci
ghci> sample2
aaa
sample2
の型はIO ()
になっています。
Hogeが未定義だった時の処理は埋め込まれてしまい、後で制御ができなくなってしました。
最初の例ではIO
とMaybe
の2つのコンテナを活用していて、相互に利用していると絡まってしまいました。
ふたつめの例複雑さを抑えるためにMaybe
を排除していましました。
3つめの方法では IO
とMaybe
をくっつけて、 MaybeT IO
を作ってみます。
import System.Environment
import Data.Maybe
import Control.Monad.Trans.Maybe
sample3 = do
hoge <- MaybeT $ lookupEnv "Hoge" -- IO (Maybe String)を MaybeT IO Stringに変換した。箱は一つなので <- で値をひっぱてくると Stringになっている
lift $ putStrLn hoge -- IO () なので MaybeT IO () に変換
$ stack ghci
ghci> runMaybeT sample3 -- IO (Maybe String)になるので実行できて、 Maybe Stringになる
Nothing
ghci> runMaybeT sample3 >>= maybe (putStrLn "not found") (const return ()) -- 後から処理の追加もできる
"notFound"
$ Hoge=aaa stack ghci
ghci> runMaybeT sample3
aaa
ghci> runMaybeT sample3 >>= maybe (putStrLn "not found") (const return ())
aaa
sample3を使うにはrunMaybeT
を使う必要があります。
これは MaybeT IO a
をIO (Maybe a)
に戻すことができます。
IOにすることができたので実行できます。
sample3の特徴は、値がなかったときの処理を後から追加できることです。
この方法の良い所は、MaybeT IO a
に変換するのは難しくないことです。
MaybeTを使わずに、sample3と同じような動きになるように、つくるとこんな感じになりました。
sample1_5 = do
maybeHoge <- lookupEnv "Hoge"
case maybeHoge of
Just hoge -> putStrLn hoge >> return (Just ())
Nothing -> return Nothing
Maybeの中身を一回紐解いて、IOの処理をして、再度手動でMaybe化してあげないといけなくなってます。
この作業はボイラープレートになるようなのでMaybeTを使うほうがシンプルに記述できるようです。
MaybeTへの変換は、紐解く必要がなく、何かの関数をくっつけるだけです。
よくありそうな変換を表にしてみます。
xの型 | 変換方法 | 型 |
---|---|---|
String | return x | MaybeT IO String |
Maybe String | MaybeT . return $ x | MaybeT IO String |
IO String | lift x | MaybeT IO String |
String -> String | fmap x | MaybeT IO String -> MaybeT IO String |
String -> IO () | lift . x | String -> MaybeT IO () |
String -> Maybe a | MaybeT . return . x | String -> MaybeT IO a |
リンク
IO (Maybe a)というタイトルでモナド変換子について学んだことを話した - #LT駆動 - そんなこと覚えてない