M.Hiroi's Home Page

お気楽 Haskell プログラミング入門

中級編 : Applicative

Copyright (C) 2013-2021 Makoto Hiroi
All rights reserved.

はじめに

今回は Functor の強化版である「Applicative (アプリカティブ)」について説明します。

●Applicative とは?

Applicative は Functor と同様に型クラスのひとつです。Applicative の定義をコマンド :info で調べると、次のように表示されます。

ghci> :i Applicative
type Applicative :: (* -> *) -> Constraint
class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  {-# MINIMAL pure, ((<*>) | liftA2) #-}
        -- Defined in ‘GHC.Base’
instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b) => Applicative ((,,) a b)
  -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b, Monoid c) =>
         Applicative ((,,,) a b c)
  -- Defined in ‘GHC.Base’
instance Applicative ((->) r) -- Defined in ‘GHC.Base’
instance Applicative IO -- Defined in ‘GHC.Base’
instance Applicative [] -- Defined in ‘GHC.Base’
instance Applicative Maybe -- Defined in ‘GHC.Base’
instance Applicative Solo -- Defined in ‘GHC.Base’
instance Applicative (Either e) -- Defined in ‘Data.Either’

ここで、関数 pure と演算子 <*> に注目してください。この 2 つが Applicative の主な機能です。f は型クラス制約で Functor を指定されているので、Functor と同様に型変数をひとつ取る型構築子になります。関数 pure の型は a -> f a なので、任意のデータを f の中に格納して返す働きをします。

簡単な例を示しましょう。

ghci> pure 1 :: Maybe Int
Just 1
ghci> pure 1 :: [Int]
[1]
ghci> pure 1 :: Either a Int
Right 1

pure の働きは「引数を文脈に入れて返す」と考えることができます。このことを「値を持ち上げる」といいます。引数が 1 とすると、文脈が Maybe であれば Just 1 に、リストであれば [1] に、Either であれば Right 1 になります。

<*> の型は fmap とよく似ていますが、最初の関数が f に格納されているところが異なります。つまり、f に格納されている関数を取り出し、それを f に格納されているデータに適用し、その結果を f に格納して返す、という働きをします。

簡単な実行例を示します。

ghci> pure (*2) <*> Just 10
Just 20
ghci> pure (*2) <*> Nothing
Nothing
ghci> pure (*2) <*> [1,2,3,4,5]
[2,4,6,8,10]
ghci> pure (*2) <*> []
[]
ghci> pure (*2) <*> Right 10
Right 20
ghci> pure (*2) <*> Left "error"
Left "error"
ghci> pure reverse <*> getLine
hello, world
"dlrow ,olleh"

pure で関数を持ち上げます。このとき、右辺のデータ型が Maybe であれば関数は Just に格納され、リストであれば関数はリストに、Either であれば関数は Right に格納されます。あとは、fmap と同様に文脈を保ったまま関数を値に適用します。

ところで、リストには複数の関数を格納することができます。この場合、動作は次のようになります。

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

(+3) を適用して得られたリストと (*2) を適用して得られたリストを連結したものになります。

●<*> を複数回使用する

Applicative も関数の部分適用が可能です。たとえば、関数 a -> b -> c は a -> (b -> c) のことなので、これを演算子 <*> に適用すると型は次のようになります。

(<*>) :: f (a -> b) -> f a -> f b
pure (a -> b -> c) <*> => f (a -> (b -> c)) -> f a -> f (b -> c)

ここで、返り値の型は f (b -> c) になることに注目してください。これは演算子 <*> に適用できる型ですね。つまり、Applicative でカリー化関数を評価する場合、次のように引数を <*> でつなげて渡すことができます。

pure func <*> 引数1 <*> 引数2 <*> ... <*> 引数N

簡単な実行例を示します。

ghci> pure (*) <*> Just 2 <*> Just 10
Just 20
ghci> pure (+) <*> Right 2 <*> Right 10
Right 12
ghci> pure (,) <*> [1,2,3] <*> [4,5,6]
[(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]

リストの動作はリスト内包表記で表すと [(x, y) | x <- [1,2,3], y <- [4,5,6]] と同じ [*1] になります。ただし、Applicative ではリスト内包表記の条件節を実現することはできません。(,) はタプルのデータ構築子です。タプルは最大で 62 個の要素を格納できます。データ構築子は (,) だけではなく、(,,) や (,,,) など最大で 61 個のカンマ ( , ) を並べたデータ構築子が用意されています。

簡単な例を示します。

ghci> :t (,)
(,) :: a -> b -> (a, b)
ghci> :t (,,)
(,,) :: a -> b -> c -> (a, b, c)
ghci> :t (,,,)
(,,,) :: a -> b -> c -> d -> (a, b, c, d)
ghci> (,) 1 2
(1,2)
ghci> (,,) 1 2 3
(1,2,3)
ghci> (,,,) 1 2 3 4
(1,2,3,4)

ghci> pure (,,) <*> [1,2] <*> [3,4] <*> [5,6]
[(1,3,5),(1,3,6),(1,4,5),(1,4,6),(2,3,5),(2,3,6),(2,4,5),(2,4,6)]
ghci> pure (,,,) <*> [1,2] <*> [3,4] <*> [5,6] <*> [7,8]
[(1,3,5,7),(1,3,5,8),(1,3,6,7),(1,3,6,8),(1,4,5,7),(1,4,5,8),(1,4,6,7),(1,4,6,8)
,(2,3,5,7),(2,3,5,8),(2,3,6,7),(2,3,6,8),(2,4,5,7),(2,4,5,8),(2,4,6,7),(2,4,6,8)
]
-- note --------
[*1] リスト内包表記は「リストモナド」の構文糖衣です。モナドの演算子 >>= を使うと次のようになります。
ghci> [1,2,3] >>= \x -> [4,5,6] >>= \y -> return (x, y)
[(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]

●<$> と <*> の組み合わせ

pure ではなく Functor の fmap (演算子 <$>) を使って関数を持ち上げることもできます。簡単な実行例を示します。

ghci> :t (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b
ghci> (*) <$> Just 2 <*> Just 10
Just 20
ghci> (*) <$> Just 2 <*> Nothing
Nothing
ghci> (*) <$> [2] <*> [1,2,3,4,5]
[2,4,6,8,10]
ghci> (*) <$> Right 2 <*> Right 10
Right 20
ghci> (*) <$> Right 2 <*> Left "error"
Left "error"
ghci> (,) <$> [1,2,3] <*> ['a', 'b', 'c']
[(1,'a'),(1,'b'),(1,'c'),(2,'a'),(2,'b'),(2,'c'),(3,'a'),(3,'b'),(3,'c')]

Functor の演算子 <$> により関数が持ち上げられて f (a -> b) のデータ型が生成され、それが Applicative の演算子 <*> に渡されます。そして、その中て文脈を保ちながら関数 (a -> b) が評価されます。

●ZipList

ところで、Applicative におけるリストの動作はリスト内包表記と同じですが、関数 zipWith のように動作してほしい場合もあります。zipWith の動作を示します。

ghci> :t zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
ghci> :t zipWith3
zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d]

ghci> zipWith (,) "abc" [1,2,3]
[('a',1),('b',2),('c',3)]
ghci> zipWith3 (,,) "abc" [1,2,3] "def"
[('a',1,'d'),('b',2,'e'),('c',3,'f')]

Applicative でも同様の動作を行うため、モジュール Control.Applicative には ZipList というデータ型が定義されています。コマンド :info で ZipList を調べてみましょう。

ghci> :m + Control.Applicative
ghci> :i ZipList
type ZipList :: * -> *
newtype ZipList a = ZipList {getZipList :: [a]}
        -- Defined in ‘Control.Applicative’
instance Traversable ZipList -- Defined in ‘Data.Traversable’
instance Foldable ZipList -- Defined in ‘Control.Applicative’
instance Read a => Read (ZipList a)
  -- Defined in ‘Control.Applicative’
instance Alternative ZipList -- Defined in ‘Control.Applicative’
instance Applicative ZipList -- Defined in ‘Control.Applicative’
instance Functor ZipList -- Defined in ‘Control.Applicative’
instance Eq a => Eq (ZipList a) -- Defined in ‘Control.Applicative’
instance Ord a => Ord (ZipList a)
  -- Defined in ‘Control.Applicative’
instance Show a => Show (ZipList a)
  -- Defined in ‘Control.Applicative’

ZipList はリストを格納しているだけです。リストは Applicative のインスタンスになっています。それとは異なる動作をさせるため、ZipList という新しいデータ型を定義して Applicative のインスタンスに設定します。newtype はあとで説明します。なお、ZipList は Show のインスタンスではないので、インタプリタ ghci で表示する場合は関数 getZipList を使って ZipList からリストを取り出してください。

簡単な実行例を示します。

ghci> getZipList $ ZipList [1,2,3,4]
[1,2,3,4]
ghci> getZipList $ (,) <$> ZipList "abc" <*> ZipList [1,2,3]
[('a',1),('b',2),('c',3)]
ghci> getZipList $ (,,) <$> ZipList "abc" <*> ZipList [1,2,3] 
<*> ZipList "def"
[('a',1,'d'),('b',2,'e'),('c',3,'f')]

関数を ZipList に格納して、演算子 <*> で評価することもできます。

ghci> getZipList $ ZipList [(*2), (+10)] <*> ZipList [1,2]
[2,12]
ghci> getZipList $ ZipList [(*), (+)] <*> ZipList [1,2] <*> 
ZipList [3,4]
[3,6]

最初の例では 1 * 2 と 2 + 10 が評価されて [2, 12] になります。次の例では 1 * 3 と 2 + 4 が評価されて [3, 6] になります。

●newtype

newtype は data 宣言のように新しいデータ型を定義することができます。たとえば、ZipList を data 宣言で書き直して比較してみましょう。

newtype ZipList a = ZipList [a]
newtype ZipList a = ZipLIst {getZipList :: [a]}

data ZipList a = ZipList [a]
data ZipList a = ZipList {getZipList :: [a]}

data が newtype に変わっただけで、あとは同じですね。実際、data 宣言で ZipList を定義しても問題なく動作します。ただし、data 宣言を使って定義すると、Haskell が ZipList にリストを格納する処理や ZipList からリストを取り出す処理を行うことになります。これは当然のことですが、今回のようにリストとは異なるデータ型だけが必要な場合、Haskell が ZipList をリストと同じように操作できると効率的です。

newtype はこのようなときのために用意された機能です。このため、newtype ではデータ構築子は一つだけしか定義できず、そのデータ構築子の型式も一つだけしか持つことができません。そのかわり、newtype で定義された新しいデータ型は、元のデータ型と同じように一手間かけず効率的に処理することができます。

●自分で Applicative を定義する

それではここで Applicative の理解を深めるため、私たちで Applicative を定義してみましょう。型クラスの名前は Mapplicative とします。Mapplicative の定義は次のようになります。

リスト : Mapplicative の定義

class Mfunctor f => Mapplicative f where
  pure' :: a -> f a
  app   :: f (a -> b) -> f a -> f b

pure と <*> は Applicative で使用しているので、ここでは pure' と app にしました。定義は Applicative と同じです。

次は Maybe, Either, IO, リスト を Mapplicative のインスタンスに設定します。次のリストを見てください。

リスト : インスタンスの設定

-- Maybe
instance Mapplicative Maybe where
  pure' x = Just x
  app Nothing   _ = Nothing
  app (Just f)  x = fmap' f x

-- Either
instance Mapplicative (Either a) where
  pure' x = Right x
  app (Left x)  _ = Left x
  app (Right f) x = fmap' f x

-- IO
instance Mapplicative IO where
  pure' x = return x
  app action1 action2 = do
    f <- action1
    x <- action2
    return (f x)

-- リスト
instance Mapplicative [] where
  pure' x = [x]
  app fs xs = concatMap (`fmap'` xs) fs

どのデータ型でも pure' の定義は簡単ですね。Maybe であれば Just x、Either であれば Right x、IO であれば return x、リストであれば [x] を返します。app の定義も簡単です。Maybe は第 1 引数 (演算子であれば左辺) が Nothing であれば Nothing を返します。そうでなければ、左辺 Just f から関数 f を取り出して fmap' f x を評価します。f は Mfunctor なので、fmap' を使用することができます。Either の場合も同じです。

IO の場合は左辺の I/O アクション action1 から関数 f を取り出し、右辺の I/O アクション action2 から引数 x を取り出します。あとは return (f x) を返すだけです。リストの場合はちょっと複雑です。fs に格納されている関数を取り出してリスト xs に適用し、その結果を連結しないといけません。この処理は関数 concatMap を使うとうまくいきます。

それでは実際に試してみましょう。

ghci> pure (*2) `app` Just 10
Just 20
ghci> pure (*2) `app` Nothing
Nothing
ghci> Nothing `app` Just 10
Nothing
ghci> pure (*2) `app` Right 10
Right 20
ghci> pure (*2) `app` Left "error"
Left "error"
ghci> Left "error" `app` Right 10
Left "error"
ghci> [(+3), (*2)] `app` [1,2,3,4,5]
[4,5,6,7,8,2,4,6,8,10]
ghci> [(+3), (*2)] `app` []
[]
ghci> [] `app` [1,2,3,4,5]
[]
ghci> pure reverse `app` getLine
hello, world
"dlrow ,olleh"

もちろん、前回作成した型クラス Mfunctor の fmap' と組み合わせることもできます。

ghci> (*) `fmap'` Just 2 `app` Just 10
Just 20
ghci> (+) `fmap'` Right 2 `app` Right 10
Right 12
ghci> (^) `fmap'` [2,3] `app` [1..5]
[2,4,8,16,32,3,9,27,81,243]
ghci> (++) `fmap'` getLine `app` getLine
hello,
world
"hello,world"

●関数も Applicative になる

Functor で説明したように -> は中置演算子で、関数の型は (->) r a と書くこともできます。(->) r を型コンストラクタと考えると、関数の Mapplicative の定義を導くことができます。

app :: f (a -> b) -> f a -> f b
=> ((->) r (a -> b)) -> ((->) r a) -> ((->) r b)
=> (r -> a -> b) -> (r -> a) -> (r -> b)

f `app` g は関数 f :: r -> a -> b と g :: r -> a を受け取って、関数 r -> b を返します。f は r と a から b を求めます。引数 a は関数 g から求めることができるので、app が返す関数をラムダ式で書くと \r -> f r (g r) と表すことができます。

このあと、さらに `app` をつなげて関数を合成することができます。f `app` g で生成される関数の型は r -> b なので、最初に与えた引数 r が次の関数にも伝播していくことに注意してください。

プログラムは次のようになります。

リスト : インスタンスの設定 (2)

instance Mapplicative ((->) r) where
  pure' x = \_ -> x
  app f g = \x -> f x (g x)

pure' はラムダ式 (クロージャ) に x を格納して返します。app は定義どおりにプログラムしただけです。そうはいっても、これだけでは何ができるのかよくわかりませんね。具体的な例を示しましょう。

ghci> :t pure' (+)
pure' (+) :: (Mapplicative f, Num a) => f (a -> a -> a)
ghci> :t pure' (+) `app` (+10)
pure' (+) `app` (+10) :: Num a => a -> a -> a
ghci> (pure' (+) `app` (+10)) 1 2
13
ghci> (pure' (+) `app` (*10)) 1 2
12

pure' (+) `app` (+10) は 2 つの数値を引数に取る関数になります。その動作は 1 を (+10) に適用して、その結果と 2 を足し算する、つまり 2 + (1+10) = 13 となります。(*10) の場合は 2 + (1*10) = 12 になります。

ghci> :t pure' (+) `app` (+10) `app` (*10)
pure' (+) `app` (+10) `app` (*10) :: Num b => b -> b
ghci> :t pure' (+) `app` (+10) `app` (*10) $ 10
pure' (+) `app` (+10) `app` (*10) $ 10 :: Num t => t
ghci> pure' (+) `app` (+10) `app` (*10) $ 10
120

次に、`app` (*10) を追加しましょう。すると、合成された関数は引数が 1 つの関数になります。その動作は引数 10 を (+10) と (*10) に適用して、その結果を足し算する、つまり (10 + 10) + (10 * 10) = 120 になります。

pure' のかわりに fmap' を使っても同じように動作します。

ghci> (+) `fmap'` (+10) `app` (*10) $ 10
120

●ZipList の定義

もちろん、ZipList も定義することができます。次のリストを見てください。

リスト : ZipList の定義

newtype ZipList a = ZipList {getZipList :: [a]}

instance Mfunctor ZipList where
  fmap' f (ZipList xs) = ZipList (map f xs)

instance Mapplicative ZipList where
  pure' x = ZipList (repeat x)
  app (ZipList fs) (ZipList xs) = ZipList (zipWith id fs xs)

プログラムは Haskell のモジュール Data.Applicative とほとんど同じです。Mfunctor の fmap' の定義は簡単ですね。Mapplicative の演算子 app は zipWith を使うと簡単です。id には fs の要素 fi と xs の要素 xi が次のように渡されます。

id f x => (id fi) xi => fi xi

つまり、xs の要素 xi に fs の要素 (関数) fi を適用していくことになります。関数 pure' はちょっと変わっていますね。repeat x は x の無限リストを生成する関数です。ここで、リスト [x] を返すと、各リストの先頭要素だけに x を適用することになるので、長さが 1 のリストしか生成されません。リストの各要素に関数 x を適用するため、repeat で無限リストを生成しています。

簡単な実行例を示します。

ghci> getZipList $ pure' (*) `app` ZipList [1..5] `app` ZipList [11..15]
[11,24,39,56,75]
ghci> getZipList $ (*) `fmap'` ZipList [1..5] `app` ZipList [11..15]
[11,24,39,56,75]
ghci> getZipList $ ZipList [(+1), (+2), (+3)] `app` ZipList [10,11,12]
[11,13,15]
ghci> getZipList $ (,,) `fmap'` ZipList [1..5] `app` ZipList [11..15] `app` 
ZipList "abcde"
[(1,11,'a'),(2,12,'b'),(3,13,'c'),(4,14,'d'),(5,15,'e')]

●liftA2

このほかに、モジュール Control.Applicative には便利な関数 liftA2 が用意されています。関数 liftA2 を Mapplicative で定義すると次のようになります。

リスト : liftA2

liftA2 :: Mapplicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f `fmap'` a `app` b

プログラムは簡単ですね。実行例を示します。

ghci> liftA2 (+) (Just 1) (Just 2)
Just 3
ghci> liftA2 (:) (Just 1) (Just [2])
Just [1,2]
ghci> liftA2 (+) [1,2,3,4] [5,6,7,8]
[6,7,8,9,7,8,9,10,8,9,10,11,9,10,11,12]

また、「簡単な入出力」で説明した関数 sequence のような関数を作ることもできます。Haskell には sequenceA という関数があるので、名前は sequenceA' としました。

リスト : sequenceA

sequenceA' :: Mapplicative f => [f a] -> f [a]
sequenceA' [] = pure' []
sequenceA' (x:xs) = (:) `fmap'` x `app` sequenceA' xs

-- 別解
sequenceA'' :: Mapplicative f => [f a] -> f [a]
sequenceA'' = foldr (liftA2(:)) (pure' [])

sequenceA' の型を見ればおわかりのように、リストに格納されている f a から a を取り出して、それをリストに格納した [a] を作り、それを f に格納して返します。プログラムは簡単にみえますが、Mfunctor と Mapplicative が働いていることに注意してください。fmap' で x からデータを取り出し、それを sequeceA' の返り値のリストに追加します。また、別解のように foldr と liftA2 を組み合わせてプログラムすることもできます。

簡単な実行例を示します。

ghci> sequenceA' [Just 1, Just 2, Just 3]
Just [1,2,3]
ghci> sequenceA' [Just 1, Just 2, Just 3, Nothing]
Nothing
ghci> sequenceA' [Right 1, Right 2, Right 3]
Right [1,2,3]
ghci> sequenceA' [Right 1, Right 2, Left "error", Right 3]
Left "error"
ghci> sequenceA' (map print [1,2,3,4,5])
1
2
3
4
5
[(),(),(),(),()]

I/O アクションも Mapplicative なので、sequenceA' を使うとリストに格納された I/O アクションを実行することができます。

リストの場合、動作がもっと複雑になります。次の例を見てください。

ghci> sequenceA' [[1,2],[3,4]]
[[1,3],[1,4],[2,3],[2,4]]

これは次のように動作します。

ghci> (:) `fmap'` [3,4] `app` [[]]
[[3],[4]]
ghci> (:) `fmap'` [1,2] `app` [[3],[4]]
[[1,3],[1,4],[2,3],[2,4]]

リストの場合、pure' [ ] は [[ ]] のことなので、3 : [ ], 4 : [ ] が評価されて、それがリストに格納されます。次に、1 と [3], [4] が結合され、2 と [3], [4] が結合され、それらがリストに格納されるので、返り値は [[1,3],[1,4],[2,3],[2,4]] になります。

sequenceA' はリストに格納された関数に引数を渡して評価することもできます。

ghci> :t sequenceA' [(+3), (*4), (^2)]
sequenceA [(+3), (*4), (^2)] :: Num a => a -> [a]
ghci> sequenceA' [(+3), (*4), (^2)] $ 5
[8,20,25]

●Applicative の規則

Applicative にも満たすべき規則があります。まず大前提として、pure f <*> x = f <$> x を満たす必要があります。この規則を満たさないと Functor と Applicative を組み合わせて使うことができません。

それでは実際に試してみましょう。

ghci> pure' (+10) `app` Just 10
Just 20
ghci> (+10) `fmap'` Just 10
Just 20
ghci> pure' (+10) `app` Right 10
Right 20
ghci> (+10) `fmap'` Right 10
Right 20
ghci> pure' (+10) `app` [1,2,3,4,5]
[11,12,13,14,15]
ghci> (+10) `fmap'` [1,2,3,4,5]
[11,12,13,14,15]
ghci> getZipList $ (+10) `fmap'` ZipList [1,2,3,4,5]
[11,12,13,14,15]
ghci> getZipList $ pure' (+10) `app` ZipList [1,2,3,4,5]
[11,12,13,14,15]

正しく動作していますね。

このほかに、Haskell の Data.Applicative のマニュアルによると、次の 4 つの規則があるそうです。

  1. pure id <*> v = v
  2. pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
  3. puer f <*> pure x = pure (f x)
  4. u <*> pure y = pure ($ y) <*> u

規則の詳しい説明は本稿の範囲を超えるので (M.Hiroi の力不足なので)、ここでは Maybe を使って、規則が成り立っていることを示すだけにとどめます。これらの規則の意味は shelarcy さんの「本物のプログラマは Haskell を使う」 "第51回 FunctorとMonadの間にあるApplicative" で詳しく説明されています。そちらをお読みくださいませ。

ghci> pure' id `app` Just 10
Just 10
ghci> pure' (.) `app` Just (*2) `app` Just (+5) `app` Just 10
Just 30
ghci> Just (*2) `app` (Just (+5) `app` Just 10)
Just 30
ghci> pure' (*2) `app` Just 10
Just 20
ghci> pure' ((*2) 10) :: Maybe Int
Just 20
ghci> Just (*2) `app` pure' 10
Just 20
ghci> pure' ($ 10) `app` Just (*2)
Just 20

初版 2013 年 3 月 10 日
改訂 2021 年 1 月 17 日