Help us understand the problem. What is going on with this article?

いろいろなモナド at Nakameguro.hs #2

いろいろなモナド at Nakameguro.hs #2

by ryota-ka
1 / 47

発表者紹介


はじめに


はじめに

前半戦 👉 モナドに至る道 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
    • 自分で定義するデータ型
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away