tak0kadaの何でもノート

発声練習、生存確認用。

医学関連は 医学ノート

「Learn You a Haskell for Great Good!」第11章を読んだ

第10章は勉強したことを実際に使ってプログラムを書く章であった。実用はともかく新しい概念を勉強したいのでまとめることは時間の関係でパスすることにする。第11章はファンクタ、アプリカティブファンクタである。

ファンクタ、アプリカティブ、モナドについてはHaskell - 箱で考えるFunctor、ApplicativeそしてMonad - Qiitaが分かりやすい。

ファンクタ

ここでファンクタの定義を書いておく

-- 定義
class Functor f where
    fmap :: (a -> b) -> f a -> f b

-- ファンクタのインスタンスの定義
instance Functor Tree where
    fmap f EmptyTree = EmptyTree
    fmap f (Node x left right) = Node (f x) (fmap f left) (fmap f right)

純粋な空間から出て実世界の値をやりとりするIOアクションもファンクタである。そこでfmapを使ってIOアクションの中身にアクセスできる。

instance Functor IO where
    fmap f action = do
        result <- action
        return $ f result

main = do
    line <- getLine
    putStrLn $ reverse line

-- 等価
main = do
    line <- fmap reverse getLine
    putStrLn line

ファンクタの簡単な喩えとして箱があるが、値を修飾するものと捉えることも出来る。その例が関数である。関数の定義に用いる記号->

instance Functor ((->) r) where
    fmap f g = (\x -> f (g x))

と定義される。これをfmapの定義に代入して見ると

fmap :: (a -> b) -> ((->) r a) -> ((->) r b)
-- 等価
fmap :: (a -> b) -> (r -> a) -> (r -> b)

これはInt -> Charの関数をとって、[Int]をとって[Char]をとるmap関数とそう変わらないし、ファンクタとしてひとくくりにする条件がfmapが定義できることにしても良さそう。fmapは(a -> b)をとって、(f a -> f b)を返すと見ることもでき、この操作を関数の持ち上げという。

fmapは2引数関数で写すことも出来る。つまり普通の関数をファンクタの中に適用できる。

> :t (++)
(++) :: [a] -> [a] -> [a]
> :t fmap (++) (Just "hey")
fmap (++) (Just "hey") :: Maybe ([Char] -> [Char])
> fmap ($9) $ fmap (*) [1, 2, 3]

ファンクタ則

ファンクタはfmapが定義できたら良さそうだと書いたが、実際にはファンクタになるには以下の2つの法則を満たさないといけない。Haskellはファンクタ則を満たすかどうかのチェックはできないので自分で確認する必要があることに注意。

  • 第1法則
fmap id := id
  • 第2法則
fmap (f . g) x = fmap f (fmap g x)

これらの法則を満たさない例としてMaybeもどき、

data CMaybe a = CNothing | CJust Int a deriving (Show)

instance Functor CMaybe where
    fmap f CNothing = CNothing
    fmap f (CJust counter x) = CJust (counter + 1) (f x)

がある。これはfmapを繰り返し適用すると、次第にIntの部分が大きくなっていってしまう。一見fmapの定義はできていて、コンパイルも通るのだが第1法則、第2法則のどちらも満たさない。

アプリカティブ(or アプリカティブファンクタ)

アプリカティブはファンクタと違ってファンクタの中の関数をファンクタの中の値に適用できる。Control.Applicativeをimportして使う。

アプリカティブの定義:

class (Functor f) => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

pureは文脈(型推論?)で指定されるアプリカティブの値にして返す。<*>はfmap :: (a -> b) -> f a -> f bに似ている。<*>(と後述の<$>)は左結合。

Maybe

以下ではMaybeやIO、リスト、関数などファンクタの性質があったものが実はアプリカティブでもあることを見ていく。

Maybeはファンクタであると同時にアプリカティブファンクタでもある。アプリカティブファンクタの定義から分かることとして、型コンストラクタfも具体型を1つ型引数にとることがわかるのでむやみに(Maybe a)として具体型にしてはいけない。

-- 間違い!!!
-- instance Applicative (Maybe a) where
instance Applicative Maybe where
    pure = Just
    Nothing <*> _ = Nothing
    (Just f) <*> something = fmap f something

これを使うと、(<$>はfmapと等価な中置関数、f <$> x = fmap f x。fmapと等価なのでアプリカティブでなくてもファンクタであれば適用できる。)

-- 等価
> Just (+3) <*> Just 5
> pure (+3) <*> Just 5
> fmap (+3) Just 5
> (+3) <$> Just 5
8

> Nothing <*> Just 4
Nothing

リスト

instance Applicative [] where
    pure x = [x]
    fs <*> xs = [f x|f <- fs, x <- xs]

Maybeの最小の「文脈」はNothingで要素を格納するためpure = Justとした。リストの場合、最小の文脈は[]で、要素を格納すれば[x]とすればいい。<*>については左辺のリストの関数を右辺との全ての組み合わせで格納する。リストを複数の要素を持つ非決定計算とみなすと、<*>は非決定性計算を組み合わせてより大きい非決定計算を作る演算と理解できる。

> [(+), (*)] <*> [1, 2] <*> [3, 4]
[4, 5, 5, 6, 3, 4, 6, 8]

Zipリスト

リストの<*>の定義を書き換えたものがZipList。

instance Applicative ZipList where
    pure x = ZipList (repeat x)
    ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)

例: (ZipListはShowのインスタンスではない)

> getZipList $ (+) <$> ZipList [1, 2, 3] <*> ZipList [100, 100, 100]
> getZipList $ (+) <$> ZipList [1, 2, 3] <*> pure 100
[100, 200, 300]
> getZipList $ max <$> ZipList [1, 2, 3] <*> ZipList [10, 10, 10, 10]
[10, 10, 10]
-- (,,)は\x y x -> (x, y, z)と等価
> getZipList $ (,,) <$> ZipList "dog" <*> ZipList "cat" <*> ZipList "rat"
[('d','c','r'), ('o','a','a'), ('g','t','t')]

IO

instance Applicative IO where
    pure = return
    a <*> b = do
        f <- a
        x <- b
        return (f x)

例:

-- 等価
myAction :: IO String
myAction = do
    a <- getLine
    b <- getLine
    return $ a ++ b

myAction :: IO String
myAction = (++) <$> getLine <*> getLine

関数

関数をアプリカティブとして使うことは少ない。

instance Appicative ((->) r) where
    pure x = (\_ -> x)
    f <*> g = \x -> f x (g x)

例:

> pure 3 [1, 3]
> pure 3 "blah"
3

-- (+3)と(*100)に引数を与えて結果を足す関数
> (+) <$> (+3) <*> (*100) $ 5
508

アプリカティブ則

  • pure f <*> x = fmap f x
  • pure id <*> v = v
  • pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
  • pure f <*> pure x = pure (f x)
  • u <*> pure y = pure ($ y) <*> u

アプリカティブを使ってみる

Control.Applicativeの関数にliftA2がある。

liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b

これは単にアプリカティブスタイルを関数にしただけだが、普通の2引数関数を2つのアプリカティブ値を引数に取る関数に昇格させる関数と見ることが出来る。

以下具体例

-- 等価
sequenceA :: (Applicative f) => [f a] -> f [a]
sequenceA [] = pure []
sequenceA (x:xs) = (:) <$> x <*> sequenceA xs

sequenceA = foldr (liftA2 (:)) (pure [])

このsequenceAはアプリカティブ値のリストをとって、リストを返り値に持つアプリカティブを返す。例えば、

> sequenceA [Just 1, Just2]
Just [1, 2]
> sequenceA [(+1), (*3), (-1)] 3
[4, 9, 2]

-- 非決定性計算
> sequenceA [[1, 2], [3, 4]]
> [[x,y] | x <- [1, 2], y <- [3, 4]]
[[1, 3], [1, 4], [2, 3], [2, 4]]

> sequenceA [getLine, getLine, getLine]
a
b
c
["a", "b", "c"]