「Haskellのモナド完全に理解した」試験問題
Haskellは勉強したけどモナドを本当に理解したって言えるのか自信がない…
\そんな人向けの試験問題を作りました!/
これから実施する試験問題を、10問中8問正解すればあなたはHaskellのモナドを完全に理解しています。私が保証します!
それではさっそく〜〜
第一問
まずは緊張をほぐしましょう。
Haskellの
Monad
は○○○○である
○○○○に当てはまるのは以下の選択肢のうちどれでしょう?
- 型
- 関数
- 型クラス
- 型シノニム
答え
3. 型クラス
これは簡単ですね!
Haskellのモナドは単なる型クラスです。
第二問
class A t => B t where
...
このような型クラス制約の関係を
のようなグラフで表すとします。この時(GHC 9.0.1において)
XXXX に当てはまる型クラス、つまりMonad
の型クラス制約は何でしょう?
Pointed
Applicative
Alternative
Traversable
答え
2. Applicative
これは余談ですが、実は Haskell 2010 Language Report では Monad
に型クラス制約はありません。今の Applicative の型クラス制約は Functor-Applicative-Monad Proposal によって GHC 7.10 から実装されたものです。
第三問
以下の型クラスの定義で???
となっているbind演算子の型は何でしょう?
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> (f a -> f b)
class Applicative m => Monad m where
(>>=) :: ???
m a -> m a -> m a
(a -> m a) -> m a
(a -> m b) -> f a -> m (f b)
m a -> (a -> m b) -> m b
答え
4. m a -> (a -> m b) -> m b
1はMonadPlus
、2はMonadFix
、3はTraversable
のメソッドの型です。
第四問
モナドは型クラスですがその実装はモナド則と呼ばれる関係を満たすことが暗に要請されます。
モナド則は以下の2つと
1. pure x >>= f === f x
2. m >>= pure === m
あと一つは何でしょう?(ただし===
は左右の値が等しいことを表します)
(m >>= f) >>= g === m >>= (\x -> f x >>= g))
pure id >>= (\f -> m >>= (\x -> f x)) === m
pure (g . f) >>= (\h -> m >>= (\x -> h x)) === (pure g >>= (\h -> m >>= (\x -> h x))) . (pure f >>= (\h -> m >>= (\x -> h x)))
u >>= (\f -> pure x >>= (\x -> f x)) === pure ($ x) >>= (\f -> u >>= (\x -> f x))
(これは捨て問かも…)
答え
1. (m >>= f) >>= g === m >>= (\x -> f x >>= g))
このモナド則があることでアクションを合成する結合順序を気にする必要がなくなるので、一部だけ関数に分けたりできることが保証されている便利な法則ですね。
2と3はFunctor則における
fmap id === id
fmap (g . f) === fmap g . fmap f
を書き換えたものです。
4も同様にApplicarive則における
u <*> pure y === pure ($ y) <*> u
を書き換えたものです。
第五問
モナドは単なる型クラスですが、Haskellではdo構文という特殊なシンタックスが用意されているという点では特別です。
ですがdo構文は単なる糖衣構文なので簡単にdo構文を使わないコードに変換することができます。
main = do
putStrLn "What is your name?"
name <- getLine
let message = "Hello " ++ name
putStrLn message
上記のコードと同じ挙動を示す正しいコードは以下のうちどれでしょう。
1. main = putStrLn "What is your name?" >>= getLine (\name -> let message = "Hello " ++ name in putStrLn message)
2. main = putStrLn "What is your name?" >>= getLine >>= \name -> let message = "Hello " ++ name in putStrLn message
3. main = putStrLn "What is your name?" >>= \_ -> getLine >>= \name -> let message = "Hello " ++ name >>= \_ -> putStrLn message
4. main = putStrLn "What is your name?" >>= \_ -> getLine >>= \name -> let message = "Hello " ++ name in putStrLn message
答え
4. main = putStrLn "What is your name?" >>= \_ -> getLine >>= \name -> let message = "Hello " ++ name in putStrLn message
他の選択肢はエラーになり実行できません。
第六問
同じdo構文の中では同じモナドの関数しか使うことはできません。
以下のコードにおける2つのdo構文はそれぞれ何のモナドのdo構文でしょう?
- ① Maybe, ② Either
- ① Either, ② IO
- ① Either, ② Maybe
- ① Identity, ② Maybe
答え
3. ① Either, ② Maybe
コード中のdo構文が何のモナドのdo構文なのか意識するようになると、モナドがどんどん文脈のように見えてきますね。
第七問
リストモナドに関する問題です。
0から9の4つの数字で加減乗除(+, ×, -, ÷)により10が計算できる組み合わせを列挙するプログラムを考えましょう(車のナンバープレートで暇な時にやるやつですね)
make10
で使われている guard
はリストモナドの中で使用され、第一引数の真偽値がTrueの時だけそれ以降の処理が実行される関数です。
以下のプログラムを完成させるために guard
を実装してください。
guard :: Bool -> [()]
guard = undefined
make10 :: [[Double]]
make10 = do
x <- [0..9]
op1 <- [(+), (*), (-), (/)]
y <- [0..9]
op2 <- [(+), (*), (-), (/)]
z <- [0..9]
op3 <- [(+), (*), (-), (/)]
w <- [0..9]
let result = x `op1` y `op2` z `op3` w
-- result が10に一致しなければこれ以降の処理は実行されない
guard $ result == 10
pure [x, y, z, w]
main :: IO ()
main = mapM_ print (take 5 make10)
実行するとこのように動きます。
> runhaskell Main.hs
[0.0,0.0,1.0,9.0]
[0.0,0.0,2.0,8.0]
[0.0,0.0,2.0,5.0]
[0.0,0.0,3.0,7.0]
[0.0,0.0,4.0,6.0]
実装が正しいか確認する時はRepl.itを利用するのが手っ取り早いのでぜひ活用してください
https://replit.com/new/haskell
答え
guard :: Bool -> [()]
guard False = []
guard True = [()]
ちなみに guard
はリストだけでなくAlternative
という型クラスに関する関数として一般化することができます。
guard :: Alternative f => Bool -> f ()
第八問
モナドの中でも難解な部類に入るStateモナドを何も見ずに実装してください!
下記コードの undefined
になってる部分を埋めてください。
newtype State s a = State { runState :: s -> (a, s) }
instance Functor (State s) where
fmap f (State g) = State $ \s ->
let ( a, s') = g s
in (f a, s')
instance Applicative (State s) where
pure a = State $ \s -> (a, s)
(State f) <*> (State g) = State $ \s ->
let ( h, s' ) = f s
( a, s'') = g s'
in (h a, s'')
instance Monad (State s) where
(State f) >>= m = undefined
迷った時はStateモナドにおける(>>=)
の型を書いてみましょう。
答え
instance Monad (State s) where
(State f) >>= m = State $ \s ->
let (a, s') = f s
in runState (m a) s'
Applicativeの実装に比べると簡単ですね!
第九問
全く知らない型が出てきたとしても、その型が Monad
のインスタンスになっていればある程度使い方が分かることもあります。
以下ではRedisと呼ばれるDB(Key-Value ストア)を考えましょう。Redis は辞書(Map)というデータ構造のように特定のKeyに紐づけて値(Value)を保存することができるDBです。
- Redis に値を保存する関数
- Redis から値を取得する関数
として以下のような関数が与えられてるとします。
-- | Redis に値を保存する関数
set :: String -- 値のKey
-> String -- 保存する値
-> Redis ()
-- | Redis から値を取得する関数
get :: String -- 値のKey
-> Redis (Maybe String) -- 取得した値(Keyが存在しなければ Nothing が返る)
そして Redis
という型は以下のように Monad
のインスタンスであることがドキュメントより確認できているとします。
この時、上記の関数を利用して以下のような振る舞いをする関数を実装してください。
- Redis から
"foo"
という Key の値を取得する - 値が存在すればその値を、存在しなければ
"hoge"
という値を"bar"
という Key に保存する
example :: Redis ()
example = do
...
答え
example :: Redis ()
example = do
mvalue <- get "foo"
case mvalue of
Just value -> set "bar" value
Nothing -> set "bar" "hoge"
Monad
のインスタンスであることが分かれば型の詳細を知らなくても、その型の値と関数を合成する方法が分かるのです。
この問題は hedis という実際のライブラリを元に作成しています。
実際、このようにして作った example
関数は
runRedis :: Connection -> Redis a -> IO a
という関数を利用してIO
に変換し実行することができます。Connection
は接続情報などが含まれているデータ型です。
第十問
base にある Control.Monad
モジュールでは以下のような関数が提供されています。
join :: Monad m => m (m a) -> m a
この関数は (>>=)
を使って
join m = m >>= id
のように実装することができます。
反対に、先に join
が与えられたとすると join
を使って (>>=)
すなわち Monad
のインスタンスを実装することができます。
join
を使って (>>=)
を実装してみてください。ただし Functor
や Applicative
のメソッドは使っても良いこととします。
instance Monad m where
m >>= k = undefined
where
join :: m (m a) -> m a
join = {- この実装は既知とします -}
答え
instance Monad m where
m >>= k = join (fmap k m)
where
join :: m (m a) -> m a
join = {- この実装は既知とします -}
このように (>>=)
と join
は片方が定義されていれば、もう片方の実装は自動的に導かれます。
原理的には Monad
のメソッドとして (>>=)
ではなく join
を採用することもできるのです。
モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?
という言い回しがありますが join
はこの モノイド対象
に深く関わってくる関数でもあります。
これにて試験終了です!いかがだったでしょうか?
10問中8問正解していればHaskellのモナドを完全に理解しています!
日常生活で Monad
を使う上で何にも困らないでしょう👀
残念ながら8問解けなかった人、別に何か起こるわけでもないので気にしないでください😅
あくまでこの記事は難解と良く言われるHaskellのモナドを理解できているかの目安に使っていただけるとありがたいので、もし勉強不足を感じていたら復習して再チャレンジしてみてください。世の中には数多のモナドチュートリアルが存在するので勉強のネタには困らないでしょう。入り口がわからない人はこちらのリンク集も参考にしてください。
8問以上正解できた人おめでとうございます!
「完全に理解した」からさらに先にある「何もわからん。チョットワカル」を目指して頑張ってください👏(言わずもがなですがここらへんの言い回しは「ダニング=クルーガー効果」を参照)
\読んでいただきありがとうございました!/
この記事は「第87回Haskell-jpもくもく会 @ オンライン」の時間を利用して書かれました。
Haskell-jpでは2021年11月7日に Haskell Day 2021 というオンラインイベントも開催するのでチェックしてみてください!
この記事が面白かったら いいね♡ をいただけると嬉しいです☺️
バッジを贈っていただければ次の記事を書くため励みになります🙌
Discussion