アクションのmはなんのm?
2019/12/3
はじめに
Haskellのモナドについて調べていると、時々「アクション」というものが説明なしに出てくることがありました。コレはなんだろう、普通の関数と違うものなのか?調べていくと恐らくm aだな、という検討はついたのですが、じゃあこのmはただの型コンストラクタでもいいのか?などの疑問が沸き起こってきました。 ということで、このmには実は制約があるのか?などについて調べてみました。
...
みたいな疑問が、一ヶ月ぐらい前に沸き起こってまとめてとっさにアドカレに登録したんですが、今見返すと当たり前のことしか言ってないので、なんだか微妙な気持ちなのですが、もしかしたら同じ疑問を持つ入門者の方もいらっしゃるかもしれないので、このまま公開しようと思います。
Haskellガチ勢の方は生暖かい目で眺めていただけると幸いです。また、言葉の定義などがあやふやな箇所もあるかもしれないので、用語の使い方がおかしい点がございましたら、@mrsekutにリプを飛ばしてもらえると嬉しいです。 tl;dr
m aのmはモナド
アクションとはモナド値m aのこと
追記補足あり
想定読者
モナドについてなんとく知っている
do記法についてなんとなく知っている
アクションのおさらい
「アクション」はdo記法の話のときに出てくるコレのことでした。 code:hs
ac = do
x <- action1 -- ←コレ
y <- action2 -- ←コレ
return (x > y)
このコードに、明示的に型を書くならこんな感じになるでしょうか。
以下のコードは動かないことに注意です。
code:hs
ac :: m c
ac = do
x :: a <- action1 :: m a
y :: b <- action2 :: m b
(return (x > y)) :: m c
ここから以下のようなことが窺い知ることができます。
do式の中では<-を使うことでアクションm aから中身のaを取り出せる
一つのdo式の中で扱えるアクションはmが同じものである必要がある
m aのaのほうの型は異なっていても良い
returnでは値を再びmで包んだものを返している
do式全体はアクションm cになっている
do式全体の型はdo式の最後の行のアクションの型に一致している
もっとわかりやすく具体的な型を用いて書いてみましょう。
例えばm aをMaybe Int型として書いてみると、以下のようになります。
code:hs
ac :: Maybe Bool
ac = do
x :: Int <- action1 :: Maybe Int
y :: Float <- action2 :: Maybe Float
(return (x > y)) :: Maybe Bool
このコードのノリで、実際に動くものを書いてみると以下のようになりました。
code:hs
ac :: Maybe Bool
ac = do
x <- Just 2
y <- Just 3.0
return (x > y)
main :: IO ()
main = print ac -- Maybe False
ただの型コンストラクタで試してみる
do式とアクションの扱いはわかったので、さっそく本題のm aのmは何か?を一歩ずつ確認していきます。
最も簡単なものはただの型コンストラクタを自分で定義してみることでしょうか。
こんな型を用意します。
code:hs
data MyMaybe a = MyJust a | MyNothing
Maybe型風の自作型です。
本来のMaybe型と異なるのは、なんの型クラスも実装していない点です。
このMyMaybeを使って先程の例のように、do式の中で触ってみます。
code:hs
data MyMaybe a = MyJust a | MyNothing
ac = do
x <- MyJust 2
y <- MyJust 3
return $ x + y
このファイルをghciで読み込んでみると、以下のようなエラーが出ました。
code:err
*Main> :l myMaybe.hs
1 of 1 Compiling Main ( prac.hs, interpreted ) myMaybe.hs:4:5: error:
• No instance for (Monad MyMaybe) arising from a do statement
• In a stmt of a 'do' block: x <- MyJust 2
In the expression:
do x <- MyJust 2
y <- MyJust 3
return $ x + y
In an equation for ‘ac’:
ac
= do x <- MyJust 2
y <- MyJust 3
return $ x + y
|
4 | x <- MyJust 2
| ^^^^^^^^^^^^^
Failed, no modules loaded.
上のエラーの1行目でx <- MyJust 2に対して、No instance for (Monad MyMaybe) arising from a do statementと言われています。
これは、MyMaybe型はMonad型クラスのインスタンスではないのでdo式の中では使えないぞ、と怒っているのですね。
どうやら、最初の疑問の答えは出たようです。
そう、アクションm aのmはどうやらMonadのmっぽいです。
Monad型クラスのおさらい
ここで、少しMonad型クラスのおさらいをしておきましょう。
型Aをモナドとして扱いたければ、型AをMonad型クラスのインスタンスにする必要があります。
また、Monad型クラスのインスタンスにするためには、Applicative型クラスのインスタンスにする必要があり、
また、そのためにはFunctor型クラスのインスタンスにする必要がありました。
要するに、自作の型Aをモナドにするためには、以下の3つの型クラスのインスタンスに順にしていく必要があります。
1. Functor型クラス
2. Applicative型クラス
3. Monad型クラス
Functor型クラスのインスタンスだけにしてみる
では、話を戻します。
先程、自作のMyMaybe型を作りましたが、これはなんの型クラスのインスタンスでもありませんでした。
そして、do式の中で使おうとするとMonadのインスタンスじゃないから無理やぞ、と怒られました。
すでに答えは見えましたが確認のため、MyMaybeにFunctor型クラスのみを実装して試してみましょう。
code:hs
data MyMaybe a = MyJust a | MyNothing
-- Functor型クラスのインスタンスにする
instance Functor MyMaybe where
fmap f (MyJust x) = MyJust (f x)
fmap f MyNothing = MyNothing
ac = do
x <- MyJust 2
y <- MyJust 3
return $ x + y
これで、再び試してみます。
すると、先程と全く同じエラーが出ました。
ということで、MyMaybeをMonadの仲間入りさせましょう。
Monadのインスタンスになるためには、Applicativeのインスタンスでもある必要があるので、以下のように実装します。
これは本来のMaybe a型のときと同じ定義にしています。
code:hs
data MyMaybe a = MyJust a | MyNothing
instance Functor MyMaybe where
fmap f (MyJust x) = MyJust (f x)
fmap f MyNothing = MyNothing
instance Applicative MyMaybe where
pure = MyJust
MyNothing <*> _ = MyNothing
(MyJust f) <*> something = fmap f something
instance Monad MyMaybe where
return x = MyJust x
MyNothing >>= f = MyNothing
MyJust x >>= f = f x
ac = do
x <- MyJust 2
y <- MyJust 3
return $ x + y
これを読み込んでみると、予想通り、エラーが出ませんでした。
アクションとは
これで、アクションm aのmはMonadのmであることがわかりましたが、実際問題「アクションのmはMonadのmだよ」ってどこかに定義されているのでしょうか。
つまり、ghciで:tして、Monadの型制約があるよ、というのを確認したいのです。
というのも、mrsekut.iconがモナドについて調べてる時、急にdo式の話をされ、さぞ知っているのが当たり前かのように「アクション」という単語が出てきました。
mrsekut.iconはこの「アクション」と「関数」の違いがわからず、「???」ってなったので、とにかく信頼元の定義が知りたいのです。
そこでdoの型を調べればよいのでは?と思い試してみました。
code:shell
Prelude> :t do
<interactive>:1:1: error: Empty 'do' block
Oh...
doは関数じゃなかった。
doとbindの関係性のおさらい
途方に暮れかけていましたが、ここで「do式はbindの糖衣構文」というのを思い出します。
bindとは(>>=)のことです。
さきほど、Monad型クラスのインスタンスにする時に出てきましたね。再掲します。
code:hs
instance Monad MyMaybe where
return x = MyJust x
MyNothing >>= f = MyNothing -- ←こいつ
MyJust x >>= f = f x -- ←こいつ
bindの型を確認してみましょう。
code:shell
Prelude> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
モナド値m aと
普通の値を引数にとって、モナド値を返す関数a -> m a
の2つを引数にとって、モナド値m bを返すんですね
do式とbindがどんな風に糖衣構文なのかを確認します。
これと、
code:hs
a x = do
y <- Just x
Just y
これは、同じです。
code:hs
a' x = Just x >>= Just
1行前で取り出した値を次の行で、再びJustで括って返しています。
1行前でなくても頑張ればいけます。
例えば、これと
code:hs
a = do
x <- Just 4
y <- Just 10
return $ x + y
これは、同じになります。
code:hs
aa' = Just 4 >>= (\x -> Just 10 >>= (\y -> return (x + y)))
bindの方は、ラムダ式も入ってきてパッと見、わかりにくいですね。
それもそのはずで、本来はbindだけでも全く同じことができていましたが、より見やすく、考えやすく扱うために糖衣構文であるdoがあるのです。
アクションとは、再び
脱線しましたが、話を戻します。
何がしたかったかというと、アクションってなんだよ、の明確な解答が欲しかったのです。
do式の中の右辺で使っているやつがアクションであり、糖衣構文なのでそれはbindの第一引数になるということを確認しました。
つまり、アクションとは、「bindの第一引数として取れるやつ」ということになります。
なので結局Monad m => m aになるわけですね。
この記事中で雑に「モナド値」と呼んでたそれがアクションのようです。
じゃあ、Monad m => a -> m aのような関数があったら、これはなんと呼ぶのでしょうか。
「アクションを返す関数」です。
微妙に残る疑問点
mrsekut.iconは記事の流れそのままでアクションについて考えていたのですが、結局「アクションとはこういうものだよ」という公式の文献に当たれていません。
識者にお聞きしたいのは以下の点です。
「アクション」という言葉は公式に定義されているのか
ちょっとググると「IOアクション」が沢山出てくるが、これはなぜか
なぜIOモナドの値のみが特別扱いされているのでしょうか。最初「アクション」とはIOアクションだけに関する用語なのかなと誤認をしましたが、慣習なのでしょうか。
追記
詳しくは、このツイートを見ていただけるとわかりやすいかと思いますが、個人的なメモを下に残しておきます。
広義の意味では、「アクション」は逐次的に実行するもの
Haskellでは、式の評価順序は定められていない
しかし、副作用を扱うIOでは、評価の順序が結果に作用するので、順序を規定する必要がある
評価順序の決まっていないHaskell界の「関数」と、評価順序の決まっているソレを区別するために、後者を「アクション」と呼んでいる
なので、「アクション == モナド値」というのは若干微妙だが、文脈に依存する機構であるモナドの中で扱うものなので、部分的には正しい、と思う
しかし、例外もあるようで、モナドでない箇所でも逐次処理が必要な場合、それを「アクション」と呼ぶこともあるらしい
え、じゃあm aのmはモナドじゃないこともあるのか..mrsekut.icon
たまに「モナドとは純粋関数の世界で副作用を扱うもの」という説明を見かけることがある気がするが、全てのモナドが副作用を扱うわけではない
副作用を扱うもので顕著なのがIOモナド
なので、「IOアクション」がよく目につく感じになっている
結論
以上を踏まえたまとめです。
アクションm aのmは(多くの場合?)モナドのmです。
また、アクションとは、順序の規定された関数のようなもの、ということになります。
結論があやふやな記事になってすみません、何かのお役に立てれたら幸いです。