今回は Functor の強化版である「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) ]
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) が評価されます。
ところで、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 は 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 を定義してみましょう。型クラスの名前は 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"
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 の定義 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')]
このほかに、モジュール 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 にも満たすべき規則があります。まず大前提として、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 つの規則があるそうです。
規則の詳しい説明は本稿の範囲を超えるので (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