Haskell Monad(モナド)とは?


まとめ

  1. Functor:関数は圏外にあるが、引数の値が圏内であれば<$> (fmap)が使える。
  2. Applicative:関数も引数の値も同じ圏内であれば<*>が使える。functorクラス型圏内に値(関数)を所属させるにはpureが使える。
  3. Monad:メソッドチェーンなどで戻り値は圏内だが、 関数は引数の値が圏外にある時には>>=が使える。returnは値をmonadクラス型圏内に所属させる際に使う。

詳細

Monadを理解するにはHaskellについてここまで学んだことを総動員する必要があると感じたのでそれらもついでに整理。

シンプルな型

カインド(種):*

概念図

関数

型を引数にとり何らかの処理を行った結果を型にして返す

型シグネチャの例
関数A : f Int -> String 関数B : f String -> Int
関数A : f Int -> Int 関数B : f String -> String

概念図

圏<コンテキスト>

ある型が同じ振る舞いや特徴を持つとき、その範囲を圏と呼ぶ
型引数を持つデータ型は型引数の圏

圏<コンテキスト>の例
リスト: [Int] データ型: type Data = Data Int | String
リストの値はリスト圏の値であり、データ型の値はデータ型圏の値。

概念図

コンテナとコンテキスト

データ型を指してコンテナと表現することがある。
コンテナに関数を適用した場合、関数で定義されたデータ型を引数にして演算が行われる。
一方、コンテキストに関数を適用した場合、コンテナ(データ型)が持つ型変数を引数にして演算が行われる。

例えばコンテナとしてのリストの足し算はリストの連結を意味するがコンテキストとしてのリストの足し算はリストの要素同士の足し合わせた値を返す

-- コンテナとしてのリストの例
Prelude> "Hello" ++ ", " ++ "world."
"Hello, world."

-- コンテキストとしてのリストの例
Prelude> pure (+) <*> [1..5] <*> [6..10]
[7,8,9,10,11,8,9,10,11,12,9,10,11,12,13,10,11,12,13,14,11,12,13,14,15]

1. Functor (関手)

定義:<$>(fmap):: Functor f => (a -> b) -> f a -> f b
圏の値<型変数>に関数を適用する

Functorの例
1. Functor型クラスのインスタンスaが引数
2. (a -> b):aに関数(->)を適用した結果bが生成
3. Functor型クラスのインスタンスに処理結果bを詰め込み返却する
例1) リストから値を取り出して処理を行い、別のリストへ値を詰め結果を返す
例2) Maybe型から値を取り出し処理を行い、Maybe型へ詰め結果を返す

概念図

2. Applicative(アプリカティブ)

定義:<$>(fmap):: Functor f => (a -> b) -> f a -> f b
圏の値<型変数>を取り出して関数を適用後、結果を圏に詰め込む
※Functorで定義されたメソッド

定義:<*>:: Applicative f => f (a -> b) -> f a -> f b
圏の値<型変数>に関数を適用する

定義:pure:: Applicative f => a -> f a
型をFunctorの圏に詰め込む

Applicativeの例
1. Applicative型クラスのインスタンスaが引数
2. f (a -> b):引数と戻り値がApplicative型の関数を適用した結果bを生成
3. Applicative型クラスのインスタンスに処理結果bを詰め込み返却する

Prelude> -- f (a -> b)型の関数を定義
Prelude> func1 = (^) <$> Just 4
Prelude> :t func1
func1 :: (Integral b, Num a) => Maybe (b -> a)
Prelude> -- f a を渡して処理を実行
Prelude> func1 <*> Just 2
Just 16
Prelude> -- Functorの圏に詰め込む
Prelude> functor = pure 6
Prelude> :t functor
functor :: (Applicative f, Num a) => f a
Prelude> -- pureと<*>を使った式
Prelude> pure (^2) <*> Just 4
Just 16
Prelude> -- 練習1
Prelude> (++) <$> Just "Say," <*> Just "Hello"
Just "Say,Hello"
Prelude> -- 練習2
Prelude> pure (+) <*>  [1,2] <*>  [3,4]
[4,5,5,6]
Prelude> -- 練習3
Prelude> pure (+) <*> (pure (+) <*> Just 4 <*> Just 3)  <*> Just 3
Just 10

3. Monad(モナド)

定義:<$>(fmap):: Functor f => (a -> b) -> f a -> f b
圏の値<型変数>に関数を適用する
※Functorで定義されたメソッド

定義:<*>:: Applicative f => f (a -> b) -> f a -> f b
圏の値<型変数>を取り出して圏の関数を適用後、結果を圏に詰め込む
※Applicativeで定義されたメソッド

定義:pure:: Applicative f => a -> f a
型をFunctorの圏に詰め込む
※Applicativeで定義されたメソッド

定義:>>= :: Monad m => m a -> (a -> m b) -> m b
圏から値を取り出して関数を適用した結果を圏につめこむ

定義:return :: Monad m => a -> m a
値を圏に詰め込む

定義:>> :: Monad m => m a -> m b -> m b

演習

import qualified Data.Map as Map

type UserName      = String
type GamerId       = Int
type PlayerCredits = Int

-- このMapはGameIdに紐づくUserNameを持つ
userNameDB :: Map.Map GamerId UserName
userNameDB = Map.fromList [(1, "Asano Aiko")
                         , (2, "Katase Kaito")
                         , (3, "Sato Satoshi")
                         , (4, "Tanabe Kaede")
                         , (5, "Nanase Nanako")]

-- このMapはUserNameに紐づくクレジット情報を持つ
creditsDB :: Map.Map UserName PlayerCredits
creditsDB = Map.fromList [("Asano Aiko", 5000)
                        , ("Katase Kaito", 15000)
                        , ("Sato Satoshi", 3000)
                        , ("Tanabe Kaede", 152500)
                        , ("Nanase Nanako", 6500)]


-- GamerIdに紐づくデータを取得
lookupUserName :: GamerId -> Maybe UserName
lookupUserName gamerId = Map.lookup gamerId userNameDB

-- UserNameに紐づくデータを取得する
lookupCredits :: UserName -> Maybe PlayerCredits
lookupCredits userName = Map.lookup userName creditsDB

-- GamerIdからユーザーのクレジット情報を取得する関数
creditsFromGamerId :: GamerId -> Maybe PlayerCredits
creditsFromGamerId gamerId =   lookupUserName gamerId >>= lookupCredits
-------------------------
-- Maybeを使わないでGamerIdからユーザーのクレジット情報を取得する関数
lookupCreditsWithoutMonad :: Maybe UserName -> Maybe PlayerCredits
lookupCreditsWithoutMonad Nothing = Nothing
lookupCreditsWithoutMonad (Just userName) = lookupCredits userName

creditsFromGamerIdWithoutMonad :: GamerId -> Maybe PlayerCredits
creditsFromGamerIdWithoutMonad gamerId = lookupCreditsWithoutMonad $ lookupUserName gamerId
*Main> :l monad-sample.hs
*Main> -- Monadを使わないで実装
*Main> creditsFromGamerIdWithoutMonad 1
Just 5000
*Main> creditsFromGamerIdWithoutMonad 100
Nothing
*Main> -- Monadを使って実装
*Main> creditsFromGamerId 5
Just 6500
*Main> creditsFromGamerId 50
Nothing

感想

Monadで定義されている関数がどんな意味を持ち、どんな動きをするのか概要レベルだが把握することができた。
ただし使いこなすには慣れが必要だとも感じた。