発表者紹介
- 亀岡 亮太
-
株式会社HERP リードエンジニア
- Haskell とか TypeScript とか
- We're hiring!
- GitHub @ryota-ka
- Twitter @ryotakameoka
はじめに
はじめに
前半戦 👉 モナドに至る道 at Nakameguro.hs #2
モナドは型クラスのひとつです。型クラスは、異なるデータ型の間に見られる共通の性質を抽象化するためのものでした。モナド自体の性質を睨み続けるのではなく、
Monad
型クラスのインスタンスになっているMaybe
Either e
[]
Reader r
などの具体的なデータ型を紹介し、モナドに対する気持ちを醸成することを目指します。
-
Monad
は型クラスのひとつだった - 型クラスは,異なる型に共通する性質を抽象化するものだった
-
Show
: 文字列にできる -
Ord
: 大小関係の比較ができる -
Functor
: 関数を文脈の中に持っていける -
Monad
: 文脈を持った計算を繋げて書ける
-
-
Monad
のインスタンスになる「異なる型」を色々見て回って気持ちを醸成する-
繋げて書ける感を味わってもらう (
>>=
>=>
) -
潰せる感を味わってもらう (
join
)
-
繋げて書ける感を味わってもらう (
アジェンダ (話すこと)
-
Functor
-Applicative
-Monad
recap -
Maybe
-
a -> Maybe b
の繋げ方 -
Maybe (Maybe a)
の潰し方
-
-
Either e
-
a -> Either e b
の繋げ方 -
Either e (Either e a)
の潰し方
-
-
[]
-
a -> [b]
の繋げ方 -
[[a]]
の潰し方
-
-
Reader r
-
a -> Reader r b
の繋げ方 -
Reader r (Reader r a)
の潰し方
-
-
Identity a
-
a -> Identity b
の繋げ方 -
Identity (Identity a)
の潰し方
-
話さないこと
- モナドの定義・モナドとはズバリ何であるか
- モナドは単なる自己関手の圏におけるモノイド対象だよ.なにか問題でも?
- モナド則
- その他発展的なトピック
- → 懇親会で🍻
この資料内のルール
-
($)
は極力使いません-
(
)
の方が脳内でパーズしやすいので
-
- kind をできるだけ明示します
-XKindSignatures
- 最初は脳内での推論が難しいので
-
*
の代わりにType
と書きます-
*
をType
と読むのは難しいので -XNoStarIsType
import GHC.Types (Type)
-
- 型クラスのメソッドの実装に際して型を併記します
-XInstanceSigs
-
newtype
のコンストラクタの prefix にMk
を付けます- 型の名前とコンストラクタの名前が一緒だと紛らわしいので
- 冗長な
(
)
を入れています- 脳内でのパーズがしやすいように
Functor
-Applicative
-Monad
recap
Functor
-Applicative
-Monad
recap
前半のおさらいなのでサクッと
m
の kind が Type
-> Type
である必要がある
(型引数を1つ取る)
class Functor (m :: Type -> Type) where
fmap :: (a -> b) -> (m a -> m b)
class Functor m => Applicative (m :: Type -> Type) where
pure :: a -> m a
(<*>) :: m (a -> b) -> m a -> m b
class Applicative m => Monad (m :: Type -> Type) where
(>>=) :: m a -> (a -> m b) -> m b
join :: Monad m => m (m a) -> m a
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
Functor
-Applicative
-Monad
recap
(<$>) :: (a -> b) -> (m a -> m b) -- Functor
(<*>) :: m (a -> b) -> (m a -> m b) -- Applicative
(=<<) :: (a -> m b) -> (m a -> m b) -- Monad
-
(=<<)
のa -> m b
に注目-
計算をする度に
m
に包まれて返ってくる- 二重・三重・……と増えるとしんどい
- 高々一重で抑えたい
-
join :: Monad m => m (m a) -> m a
- 二重になっている
m
を一重に潰せる
- 二重になっている
-
-
計算をする度に
Maybe
Maybe
data Maybe a
= Nothing -- 値がない (ちょうど0個の値がある)
| Just a -- 値がある (ちょうど1個の値がある)
> :kind Maybe -- 👇ヨシ!
Maybe :: Type -> Type
a -> Maybe b
の繋げ方
a -> Maybe b
なる計算は,失敗するかもしれない計算だと思うことができる
- 計算が成功したとき :
Just
に包んで値を返し,値があることを表明する - 計算が失敗したとき :
Nothing
を返し,値がないことを表明する
-- n が偶数ならば,n を 2 で割った値を Just に包んで返す.
-- そうでなければ Nothing を返す
div2 :: Int -> Maybe Int
div2 n | even n = Just (div n 2) -- 偶数なら成功
| otherwise = Nothing -- 奇数なら失敗
そのような計算を繋げて書きたい!💡
-- どうやって"繋げる"?
f :: a -> Maybe b
g :: b -> Maybe c
-- どうやって"潰す"?
join :: Maybe (Maybe a) -> Maybe a
a -> Maybe b
の繋げ方
(わざとらしいが) 実用的な例
- 環境変数
PORT
を探す- 見つからなかった
-
Nothing
を返す
-
- 見つかった
- 整数としてパーズする
- 失敗した
-
Nothing
を返す
-
- 成功して
port :: Int
が得られた-
0
以上かつ65535
以下か?- はい
-
Just port
を返す
-
- いいえ
-
Nothing
を返す
-
- はい
-
- 失敗した
- 整数としてパーズする
- 見つからなかった
a -> Maybe b
の繋げ方
愚直に case ~ of ...
で書いた例
inRange :: Int -> Bool
inRange n = 0 <= n && n <= 65535
lookup :: Eq a => a -> [(a, b)] -> Maybe b
lookup = _
lookupPortFromEnv :: [(String, String)] -> Maybe Int
lookupPortFromEnv env =
let mportStr = lookup "PORT" env -- mportStr :: Maybe String
in case mportStr of -- PORT環境変数あった?
Nothing -> Nothing -- → なかった
Just portStr -> -- → あった
let mport = readMaybe portStr -- mport :: Maybe Int
in case mport of -- 整数としてパーズできた?
Nothing -> Nothing -- → できなかった
Just port -> -- → できた
if inRange port -- port :: Int
then Just port
else Nothing
a -> Maybe b
の繋げ方
>>=
を使って書き直すと
lookupPortFromEnv' :: [(String, String)] -> Maybe Int
lookupPortFromEnv' env =
lookup "PORT" env >>= (\portStr -> -- portStr :: String が既に取れている体で計算を続けられる
readMaybe portStr >>= (\port -> -- port :: Int が既に取れている体で計算を続けられる
if inRange port
then Just port
else Nothing
)
)
case ~ of ...
が消えた!
a -> Maybe b
の繋げ方
-
case ~ of ...
が消えた!- → なんで!?
Maybe
に対する Monad
のインスタンス宣言の中で,既に場合分けが書かれている
instance Monad Maybe where
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>= _ = Nothing -- Nothing の場合には,「続きの計算」を捨てて打ち止めにする
(Just x) >>= k = k x -- Just x の場合には,中の値 x を「続きの計算」に渡す
a -> Maybe b
の繋げ方
do
構文を用いて書き直すと
lookupPortFromEnv'' :: [(String, String)] -> Maybe Int
lookupPortFromEnv'' env = do
portStr <- lookup "PORT" env
port <- readMaybe portStr
if 0 <= port && port <= 65535
then Just port
else Nothing
Maybe (Maybe a)
の潰し方
-- 二重の Maybe を一重に潰す
joinMaybe :: Maybe (Maybe a) -> Maybe a
joinMaybe Nothing = Nothing
joinMaybe (Just Nothing) = Nothing
joinMaybe (Just (Just x)) = Just x
> joinMaybe Nothing
Nothing
> joinMaybe (Just Nothing)
Nothing
> joinMaybe (Just (Just 42))
Just 42
Either e
Either e
当日は飛ばします (Maybe
とだいたい一緒なので)
data Either e a
= Left e
| Right a
> :kind Either -- "Type ->" が1個多い
Either :: Type -> Type -> Type
> :kind forall e. Either e -- 👇ヨシ!
forall e. Either e :: Type -> Type
a -> Either e b
の繋げ方
-
Either e a
型の値は失敗する可能性があるかつ失敗の理由を返すことができる計算だと思うことができる-
Left err
-> 計算は失敗して,その理由はerr
だった -
Right x
-> 計算が成功して,値x
が得られた
-
- そのような計算を繋げて書きたい!💡
-- どうやって"繋げる"?
f :: a -> (Either e) b
g :: b -> (Either e) c
-- どうやって"潰す?"
joinEither :: Either e (Either e a)
a -> Either e b
の繋げ方
instance Functor (Either e) where
fmap
:: (a -> b)
-> (Either e) a
-> (Either e) b
fmap _ (Left err) = Left err
fmap f (Right x ) = Right (f x)
instance Applicative (Either e) where
pure x = Right x
(Left e) <*> _ = Left e
(Right f) <*> r = fmap f r
instance Monad (Either e) where
(Left err) >>= _ = Left err
(Right x ) >>= k = k x
Either e (Either e a)
の潰し方
joinEither :: Either e (Either e a)
joinEither (Left err) = Left err
joinEither (Right (Left err)) = Left err
joinEither (Right (Right x)) = Right x
[]
[]
0個または1個または2個または3個または…の値を持つ
data [] a
= [] -- nil
| a : [a] -- cons
> :kind [] -- 👇ヨシ!
[] :: Type -> Type
a -> [b]
の繋げ方
-
a -> [b]
は,答えの候補が複数個ある計算だと思うことができる - そのような計算を繋げて書きたい💡
-- どうやって"繋げる"?
f :: a -> [b]
g :: b -> [c]
-- どうやって"潰す"?
joinList :: [[a]] -> a
a -> [b]
の繋げ方
instance Functor [] where
fmap
:: (a -> b)
-> ([a] -> [b])
fmap f xs = map f xs
instance Applicative [] where
pure :: a -> [a]
pure x = [x]
(<*>)
:: [a -> b]
-> ([a] -> [b])
fs <*> xs = concat (map (\f -> map (\x -> f x) xs) fs)
-- [f x | f <- fs, x <- xs]
instance Monad [] where
(>>=) :: [a] -> (a -> [b]) -> [b]
xs >>= k = concat (fmap k xs)
-- concatMap k xs
a -> [b]
の繋げ方
guard :: Bool -> [()]
guard True = [()]
guard False = []
do
構文を用いた例
pythagorean :: [(Int, Int, Int)]
pythagorean = do
y <- [1..]
x <- [1..y]
z <- [1..x+y]
guard (x^2 + y^2 == z^2)
pure (x, y, z)
> take 10 pythagorean
[(3,4,5),(6,8,10),(5,12,13),(9,12,15),(8,15,17),(12,16,20),(15,20,25),(20,21,29),(7,24,25),(10,24,26)]
a -> [b]
の繋げ方
pure
と >>=
を使って書き直すと
pythagorean' :: [(Int, Int, Int)]
pythagorean' =
[1..] >>= (\y ->
[1..y] >>= (\x ->
[1..x+y] >>= (\z ->
guard (x^2 + y^2 == z^2) >>= (\() ->
pure (x, y, z)
)
)
)
)
a -> [b]
の繋げ方
pure
と >>=
を展開すると (わかりにくい……)
pythagorean'' :: [(Int, Int, Int)]
pythagorean'' =
concatMap
(\y -> concatMap
(\x -> concatMap
(\z -> concatMap (\() -> [(x, y, z)]) (guard (x ^ 2 + y ^ 2 == z ^ 2))
) [1 .. x + y]
) [1 .. y]
) [1 ..]
[[a]]
の潰し方
joinList :: [[a]] -> [a]
joinList = foldr (++) []
> joinList [[0, 1, 2], [3, 4], [5, 6, 7]]
[0,1,2,3,4,5,6,7]
Reader r
Reader r
-- 実態は r -> a という関数
newtype Reader r a = MkReader { runReader :: r -> a } -- r は後で与えられる
> :type MkReader
MkReader :: (r -> a) -> Reader r a
> :type runReader
runReader :: Reader r a -> (r -> a)
a -> Reader r b
の繋げ方
-
Reader r a
(実質r -> a
)は,あとで与えられるr
型の値を取ってこれる計算だと思うことができる
-- どうやって"繋げる"?
f :: a -> Reader r b -- 実質 a -> (r -> b)
g :: b -> Reader r c -- 実質 a -> (r -> c)
-- どうやって"潰す"?
joinReader :: Reader r (Reader r a) -> Reader r a
a -> Reader r b
の繋げ方
instance Functor (Reader r) where
fmap
:: (a -> b)
-> (Reader r a) -- だいたい r -> a
-> (Reader r b) -- だいたい r -> b
fmap f (MkReader g) = MkReader (\r -> f (g x))
instance Applicative (Reader r) where
pure
:: a
-> (Reader r) a
pure x = MkReader (\_e -> x)
(<*>)
:: (Reader r) (a -> b) -- だいたい r -> a -> b
-> (Reader r) a -- だいたい r -> a
-> (Reader r) b -- だいたい r -> b
MkReader f <*> MkReader g = MkReader (\r -> f r (g r))
instance Monad (Reader r) where
(>>=)
:: (Reader r) a -- だいたい r -> a
-> (a -> (Reader r) b) -- だいたい a -> r -> b
-> Reader r b -- だいたい r -> b
MkReader f >>= k = MkReader (\r -> runReader (k (f r)) r)
a -> Reader r b
の繋げ方
do
構文を使った例
-- Reader r (r 型の値が取れる) という環境の中で r 型の値を得る
ask :: (Reader r) r
ask = MkReader (\r -> r)
port :: Reader [(String, String)] (Maybe Int)
port = do
env <- ask -- env :: [(String, String)]
pure (lookupPortFromEnv env)
a -> Reader r b
の繋げ方
pure
と >>=
を使って書いたバージョン
port' :: Reader [(String, String)] (Maybe Int)
port' =
MkReader id >>= (\env ->
pure (lookupPortFromEnv env)
)
a -> Reader r b
の繋げ方
定義に即して書いたバージョン (読みづらい!!)
port :: Reader [(String, String)] (Maybe Int)
port =
MkReader (\r ->
runReader (\env -> -- この辺が >>= に対応
MkReader (\_ -> -- pure に対応
lookupPortFromEnv env
) r
) r
)
a -> Reader r b
の繋げ方
-- Reader r (r 型の値が取れる) という環境の中で r' 型の値を得る
asks :: (r -> r') -> Reader r r'
asks f = MkReader f
-- Rational のリストを与えられたときに,平均値を求める
avg :: Reader [Rational] Rational
avg = do
x <- asks sum
y <- asks length
let y' = fromInteger (toInteger y)
pure (x / y')
Reader r (Reader r a)
の潰し方
Reader r (Reader r a)
は実質 r -> r -> a
→ r
を2回適用すればよさそう💡
joinReader :: Reader r (Reader r a) -> Reader r a
joinReader (MkReader f) = MkReader (\r -> runReader (f r) r)
Identity
Identity
-- 実質 a
data Identity a = Identity { runIdentity :: a }
-
a -> Identity b
は何も付加的な情報が増えない"普通の"計算であると思うことができる
a -> Identity b
の繋げ方
instance Functor Identity where
fmap f (Identity x) = Identity (f x)
instance Applicative Identity where
(Identity f) <*> (Identity x) = Identity (f x)
instance Monad Identity where
(Identity x) >>= f = f x
Identity (Identity a)
の潰し方
joinIdentity :: Identity (Identity a) -> Identity a
joinIdentity (Identity (Identity x)) = Identity x
終わりに
まとめ
- どういう性質を持てば monadic なのか?を個別のデータ型について確認した
- 文脈が付与される計算が繋げられる
- 二重に付与された文脈を一重に潰せる
- なぜモナドが重要か?
- 前の計算に依存して,次の計算を決めることができる
- 複数行のプログラムを書くために必要
これから
- 今日見なかった他のデータ型についても,これからそのような見方をできるようになっていると👍
State s
IO
MaybeT m
- 自分で定義するデータ型