今回は Haskell の Functor (ファンクタ) について説明します。ファンクタというと、SML/NJ や OCaml ユーザーにはお馴染み機能だと思います。これら ML 系言語のファンクタは、ストラクチャを受け取って新しいストラクチャを生成する機能ですが、Haskell のファンクタはこれらとはまったく異なる機能です。といっても難しいことは全然なく、Haskell では単なる「型クラス」のひとつにすぎません。データ型を Functor のインスタンスに設定すると、リスト以外のデータ型でも map と同様の働きをさせることができます。
Functor の定義をコマンド :info で調べると、次のように表示されます。
Prelude> :i Functor class Functor (f :: * -> *) where fmap :: (a -> b) -> f a -> f b (<$) :: a -> f b -> f a {-# MINIMAL fmap #-} -- Defined in ‘GHC.Base’ instance Functor (Either a) -- Defined in ‘Data.Either’ instance Functor [] -- Defined in ‘GHC.Base’ instance Functor Maybe -- Defined in ‘GHC.Base’ instance Functor IO -- Defined in ‘GHC.Base’ instance Functor ((->) r) -- Defined in ‘GHC.Base’ instance Functor ((,) a) -- Defined in ‘GHC.Base’
ここで、関数 fmap に注目してください。これが Functor の主な関数です。最初の引数が関数 a -> b で、a を b に変換する関数であることがわかります。次の引数 f a は型変数 a をひとつ持っているので、f は a を格納するデータ型 (型構築子) であることがわかります。そして、最後の引数が f b なので、f a からデータを取り出し、それを関数 a -> b に適用し、その結果を f に格納して返すことがわかります。
fmap の定義は map の定義と良く似ています。
map :: (a -> b) -> [a] -> [b]
リストの型 [a] は [ ] a のことなので、次のように書き直すことができます。
map :: (a -> b) -> [] a -> [] b
つまり、fmap は汎用のマップ関数なのです。簡単な実行例を示しましょう。
Prelude> fmap (*2) [1,2,3,4,5] [2,4,6,8,10] Prelude> fmap (*2) [] [] Prelude> fmap (*2) (Just 10) Just 20 Prelude> fmap (*2) Nothing Nothing Prelude> fmap reverse getLine hello, world "dlrow ,olleh"
リストの Functor は関数 map と同じ動作になります。Maybe の Functor は、Just からデータを取り出して、それに関数を適用して返り値を Just に格納して返します。Nothing はデータを格納していないので Nothing をそのまま返します。
getLine のデータ型は IO String です。Haskell では IO も Functor のインスタンスで、IO に格納されているデータを取り出して、それに関数適用して返り値を IO に格納して返します。
なお、Haskell には fmap と同じ働きをする演算子 <$> も定義されています。
Prelude> :t (<$>) (<$>) :: Functor f => (a -> b) -> f a -> f b Prelude> :t fmap fmap :: Functor f => (a -> b) -> f a -> f b Prelude> (*2) <$> Just 10 Just 20 Prelude> (*2) <$> [1,2,3,4,5] [2,4,6,8,10]
Haskell では「文脈」という用語を使うことがあります。たとえば、Maybe の Nothing は計算が失敗したことを表わしています。Maybe は失敗するかもしれない計算を表している、と考えることができるわけです。リストにはリストの意味を考えることができますし、I/O アクションもその意味を考えることができます。このようなプログラム上の意味を文脈といいますが、ここでは値を格納している型構築子のことを文脈と考えてもらってもかまいません。
fmap は文脈が表している値に、文脈を保ったまま関数を適用する働きをします。fmap に渡される関数は文脈に依存していません。文脈に合わせて関数を適用することを「関数を持ち上げる」といいます。fmap は関数を持ち上げて文脈の値に適用し、文脈を保ったままその結果を返すわけです。ちょっと耳慣れない用語で難しく感じるかもしれませんが、実際にプログラムを作っていくうちに慣れてくると思います。
それではここで Functor の理解を深めるため、私たちで Functor を定義してみましょう。型クラスの名前は Mfunctor とします。fmap は Haskell で使われているので fmap' としました。Mfunctor の定義は次のようになります。
リスト : Mfunctor の定義 class Mfunctor f where fmap' :: (a -> b) -> f a -> f b
Mfunctor の定義は関数名が異なるだけで Functor と同じです。
次はリスト, Maybe, IO を Mfunctor のインスタンスに設定します。次のリストを見てください。
リスト : インスタンスの設定 instance Mfunctor [] where fmap' = map instance Mfunctor Maybe where fmap' f Nothing = Nothing fmap' f (Just x) = Just (f x) instance Mfunctor IO where fmap' f action = do x <- action return (f x)
リストの fmap' は map と同じです。Maybe の場合、右辺が Nothing であれば Nothing をそのまま返し、Just x であれば Just (f x) を返します。IO の fmap' は do 構文を使うと簡単に実装できます。I/O アクション action から <- でデータ x を取り出し、f x の返り値を return で IO に格納して返します。
それでは実行してみましょう。
*Main> fmap' (*2) [1,2,3,4,5] [2,4,6,8,10] *Main> fmap' (*2) [] [] *Main> fmap' (*2) (Just 10) Just 20 *Main> fmap' (*2) Nothing Nothing *Main> fmap' reverse getLine hello, world "dlrow ,olleh"
次は Either というデータ型で試してみましょう。Either の定義を示します。
リスト : Either の定義 data Either a b = Left a | Right b
Either は Maybe を改良したデータ型で、左 (Left) 右 (Right) のどちらかにデータを格納することができます。左右のデータ型は異なっていてもかまいません。通常は Maybe の Nothing の代わりに Left a を使い、Just の代わりに Right b を使います。たとえば、エラーがあったときに Left にエラーを表すデータを入れて返す、という使い方をします。
簡単な例を示します。
*Main> Right 10 Right 10 *Main> :t Right 10 Right 10 :: Num b => Either a b *Main> Left "error" Left "error" *Main> :t Left "error" Left "error" :: Either [Char] b
Either は Maybe と同じ考え方で Functor のインスタンスにすることができます。つまり、Left のデータであればそのまま返し、Right のデータであれば関数を適用します。プログラムは次のようになります。
リスト : Either の Functor instance Mfunctor (Either a) where fmap' f (Left x) = Left x fmap' f (Right x) = Right (f x)
fmap' の定義は簡単ですね。Left x ならば Left x をそのまま返し、Right x ならば Right (f x) を返します。ここで Either の型に注目してください。Either は型変数を 2 つ指定しますが、fmap' の定義は (a -> b) -> f a -> f b なので、型構築子 f で指定できる型変数はひとつだけになります。このため、instance 宣言で Either または (Either a b) と指定するとエラーになります。
Haskell の場合、型構築子における型変数の指定は、カリー化関数のように部分適用することができます。Either の Functor は Right のデータに関数を適用するので、データ型が変化することがありますが、Left のデータ型は変化しません。この場合、Either a を型構築子 f と考えればいいのです。これを図に示すと次のようになります。
fmap' :: (a -> b) -> f a -> f b => (a -> b) -> (Either a') a -> (Either a') b => (a -> b) -> Either a' a -> Either a' b
これで Right 側のデータに関数が適用され、Left 側のデータは変化しないことを表すことができます。
簡単な実行例を示します。
*Main> fmap' (*2) (Right 10) Right 20 *Main> fmap' (*2) (Left "error") Left "error"
拙作のページ データ型の定義 で作成した連想リストも Functor のインスタンスにすることができます。次のリストを見てください。
リスト : 連想リストの Functor -- 連想リストの定義 data Alist k v = Nil | Cell k v (Alist k v) deriving Show -- Functor instance Mfunctor (Alist k) where fmap' f Nil = Nil fmap' f (Cell k v next) = Cell k (f v) (fmap' f next)
連想リストの型は Alist k v なので、Either と同じようにキーはそのままとして、データ (値) に関数を適用することにします。したがって、instance 宣言では型を (Alist k) と指定します。あとは関数 f を値 v に適用して新しい連想リストを生成するだけです。
それでは実行してみましょう。
*Main> a = Cell 'a' 1 (Cell 'b' 2 (Cell 'c' 3 Nil)) *Main> a Cell 'a' 1 (Cell 'b' 2 (Cell 'c' 3 Nil)) *Main> fmap' (*2) a Cell 'a' 2 (Cell 'b' 4 (Cell 'c' 6 Nil)) *Main> fmap' (+10) a Cell 'a' 11 (Cell 'b' 12 (Cell 'c' 13 Nil))
今まで関数の型は r -> a で表していました。Haskell の場合、-> は中置演算子で、関数の型は (->) r a と書くこともできます。Either と同じように、(->) r を型構築子と考えると、次のように fmap' の定義を導くことができます。
fmap' :: (a -> b) -> f a -> f b => (a -> b) -> (->) r a -> (->) r b => (a -> b) -> (r -> a) -> (r -> b)
これは関数合成を行う演算子 (.) の定義とまったく同じです。
(.) :: (b -> c) -> (a -> b) -> a -> c
つまり、関数の Functor は関数合成と同じ動作でよいわけです。プログラムは次のようになります。
リスト : 関数の Functor instance Mfunctor ((->) r) where fmap' = (.)
簡単な実行例を示します。
*Main> fmap' (fmap' (*2) (+3)) (Just 10) Just 26 *Main> fmap' (fmap' (*2) (+3)) Nothing Nothing *Main> fmap' (fmap' (*2) (+3)) [1..5] [8,10,12,14,16] *Main> fmap' (fmap' (*2) (+3)) [1] [8] *Main> fmap' (fmap' (*2) (+3)) [] [] *Main> fmap' (fmap' (*2) (+3)) (Right 20) Right 46 *Main> fmap' (fmap' (*2) (+3)) (Left "error") Left "error" *Main> fmap' (fmap' (*2) (+3)) (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil))) Cell 'a' 26 (Cell 'b' 46 (Cell 'c' 66 Nil))
fmap' (*2) (+3) で関数を合成して関数 (x + 3) * 2 を作り、その関数をデータに適用します。Just 10 は Just 26 に、[1,2,3,4,5] は [8,10,12,14,16] に、Right 20 は Right 46 になります。もちろん、連想リストに適用することもできます。このように、Functor を定義すると、いろいろなデータ構造に関数を適用していくことができます。
Functor には満たすべき規則が 2 つあります。
最初の規則は、データに fmap id を適用した結果は id を適用した結果と等しい、という意味です。つまり、fmap は関数をデータに適用するだけで、他の操作は一切行わない、ということです。
2 番目の規則は引数を与えて考えるとわかりやすくなります。
fmap (f . g) x = fmap f (fmap g x)
fmap (f . g) を x に適用した結果は、fmap g を x に適用してから fmap f を適用した結果と同じになります。左辺は fmap を 1 回だけ、右辺は fmap を 2 回適用しています。たとえば、fmap がリストの要素を逆に並べ替えるとしましょう。当然ですが、右辺と左辺では結果が異なりますね。つまり、fmap によって要素の並び方は変更されない、ということを意味しています。
なお、Functor の規則は Haskell が自動的にチェックしてくれるわけではありません。Functor を作ったならば、規則を満たしているかプログラマ自らが確かめる必要があります。
それでは規則を満たしているか実際に試してみましょう。
*Main> fmap' id (Just 10) Just 10 *Main> fmap' id Nothing Nothing *Main> fmap' id [1,2,3,4,5] [1,2,3,4,5] *Main> fmap' id [] [] *Main> fmap' id (Right 10) Right 10 *Main> fmap' id (Left "error") Left "error" *Main> fmap' id (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil))) Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil)) *Main> fmap' id Nil Nil
最初の規則は満たしているようですね。次は 2 番目の規則を満たしているかチェックしてみましょう。
*Main> fmap' ((+2) . (*3)) (Just 10) Just 32 *Main> fmap' (+2) (fmap' (*3) (Just 10)) Just 32 *Main> fmap' ((+2) . (*3)) Nothing Nothing *Main> fmap' (+2) (fmap' (*3) Nothing) Nothing *Main> fmap' ((+2) . (*3)) (Right 10) Right 32 *Main> fmap' (+2) (fmap' (*3) (Right 10)) Right 32 *Main> fmap' ((+2) . (*3)) (Left "error") Left "error" *Main> fmap' (+2) (fmap' (*3) (Left "error")) Left "error" *Main> fmap' ((+2) . (*3)) [1,2,3,4,5] [5,8,11,14,17] *Main> fmap' (+2) (fmap' (*3) [1,2,3,4,5]) [5,8,11,14,17] *Main> fmap' ((+2) . (*3)) (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil))) Cell 'a' 32 (Cell 'b' 62 (Cell 'c' 92 Nil)) *Main> fmap' (+2) (fmap' (*3) (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil)))) Cell 'a' 32 (Cell 'b' 62 (Cell 'c' 92 Nil))
2 番目の規則も満たしていますね。
Functor で関数の部分適用を行うこともできます。この場合、部分適用で生成された関数はデータ型の中に格納されます。たとえば、関数 a -> b -> c は a -> (b -> c) のことですから、fmap に適用すると次のようになります。
fmap :: (a -> b) -> f a -> f b fmap (a -> b -> c) => (a -> (b -> c)) -> f a -> f (b -> c)
ただし、生成された関数は fmap で実行することができません。なぜなら、関数はデータ型 f の中に格納されているからです。簡単な例を示しましょう。
Prelude> :t fmap (+) (Just 2) fmap (+) (Just 2) :: Num a => Maybe (a -> a) Prelude> :t fmap (+) [1,2,3,4,5] fmap (+) [1,2,3,4,5] :: Num a => [a -> a]
最初の例は関数 (+2) が生成されますが、その関数は Maybe 型の中に格納されます。リスト [1, 2, 3, 4, 5] の場合は、関数 (+1), (+2), (+3), (+4), (+5) が生成され、それがリストに格納されます。
このような場合、文脈の値に同じ文脈にある関数を適用する処理が必要になります。型で表すと次のようになります。
f (a -> b) -> f a -> f b
Haskell では、このような処理を行う型クラス Applicative が用意されています。Applicative については次回で詳しく説明します。
今回は Functor の強化版である「Applicative (アプリカティブ)」について説明します。
Applicative は Functor と同様に型クラスのひとつです。Applicative の定義をコマンド :info で調べると、次のように表示されます。
Prelude> :i Applicative class Functor f => Applicative (f :: * -> *) where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b GHC.Base.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 Applicative (Either e) -- Defined in ‘Data.Either’ instance Applicative [] -- Defined in ‘GHC.Base’ instance Applicative Maybe -- Defined in ‘GHC.Base’ instance Applicative IO -- Defined in ‘GHC.Base’ instance Applicative ((->) a) -- Defined in ‘GHC.Base’ instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’
ここで、関数 pure と演算子 <*> に注目してください。この 2 つが Applicative の主な機能です。f は型クラス制約で Functor を指定されているので、Functor と同様に型変数をひとつ取る型構築子になります。関数 pure の型は a -> f a なので、任意のデータを f の中に格納して返す働きをします。
簡単な例を示しましょう。
Prelude> pure 1 :: Maybe Int Just 1 Prelude> pure 1 :: [Int] [1] Prelude> pure 1 :: Either a Int Right 1
pure の働きは「引数を文脈に入れて返す」と考えることができます。このことを「値を持ち上げる」といいます。引数が 1 とすると、文脈が Maybe であれば Just 1 に、リストであれば [1] に、Either であれば Right 1 になります。
<*> の型は fmap とよく似ていますが、最初の関数が f に格納されているところが異なります。つまり、f に格納されている関数を取り出し、それを f に格納されているデータに適用し、その結果を f に格納して返す、という働きをします。
簡単な実行例を示します。
Prelude> pure (*2) <*> Just 10 Just 20 Prelude> pure (*2) <*> Nothing Nothing Prelude> pure (*2) <*> [1,2,3,4,5] [2,4,6,8,10] Prelude> pure (*2) <*> [] [] Prelude> pure (*2) <*> Right 10 Right 20 Prelude> pure (*2) <*> Left "error" Left "error" Prelude> pure reverse <*> getLine hello, world "dlrow ,olleh"
pure で関数を持ち上げます。このとき、右辺のデータ型が Maybe であれば関数は Just に格納され、リストであれば関数はリストに、Either であれば関数は Right に格納されます。あとは、fmap と同様に文脈を保ったまま関数を値に適用します。
ところで、リストには複数の関数を格納することができます。この場合、動作は次のようになります。
Prelude> [(+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
簡単な実行例を示します。
Prelude> pure (*) <*> Just 2 <*> Just 10 Just 20 Prelude> pure (+) <*> Right 2 <*> Right 10 Right 12 Prelude> 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 個のカンマ ( , ) を並べたデータ構築子が用意されています。
簡単な例を示します。
Prelude> :t (,) (,) :: a -> b -> (a, b) Prelude> :t (,,) (,,) :: a -> b -> c -> (a, b, c) Prelude> :t (,,,) (,,,) :: a -> b -> c -> d -> (a, b, c, d) Prelude> (,) 1 2 (1,2) Prelude> (,,) 1 2 3 (1,2,3) Prelude> (,,,) 1 2 3 4 (1,2,3,4) Prelude> 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)] Prelude> 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) ]
Prelude> [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 (演算子 <$>) を使って関数を持ち上げることもできます。簡単な実行例を示します。
Prelude> :t (<$>) (<$>) :: Functor f => (a -> b) -> f a -> f b Prelude> (*) <$> Just 2 <*> Just 10 Just 20 Prelude> (*) <$> Just 2 <*> Nothing Nothing Prelude> (*) <$> [2] <*> [1,2,3,4,5] [2,4,6,8,10] Prelude> (*) <$> Right 2 <*> Right 10 Right 20 Prelude> (*) <$> Right 2 <*> Left "error" Left "error" Prelude> (,) <$> [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 の動作を示します。
Prelude> :t zipWith zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] Prelude> :t zipWith3 zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d] Prelude> zipWith (,) "abc" [1,2,3] [('a',1),('b',2),('c',3)] Prelude> zipWith3 (,,) "abc" [1,2,3] "def" [('a',1,'d'),('b',2,'e'),('c',3,'f')]
Applicative でも同様の動作を行うため、モジュール Control.Applicative には ZipList というデータ型が定義されています。コマンド :info で ZipList を調べてみましょう。
Prelude> :m + Control.Applicative Prelude Control.Applicative> :i ZipList newtype ZipList a = ZipList {getZipList :: [a]} -- Defined in ‘Control.Applicative’ instance Alternative ZipList -- Defined in ‘Control.Applicative’ instance Applicative ZipList -- Defined in ‘Control.Applicative’ instance Eq a => Eq (ZipList a) -- Defined in ‘Control.Applicative’ instance Functor ZipList -- Defined in ‘Control.Applicative’ instance Ord a => Ord (ZipList a) -- Defined in ‘Control.Applicative’ instance Show a => Show (ZipList a) -- Defined in ‘Control.Applicative’ instance Read a => Read (ZipList a) -- Defined in ‘Control.Applicative’ instance Foldable ZipList -- Defined in ‘Control.Applicative’ instance Traversable ZipList -- Defined in ‘Data.Traversable’
ZipList はリストを格納しているだけです。リストは Applicative のインスタンスになっています。それとは異なる動作をさせるため、ZipList という新しいデータ型を定義して Applicative のインスタンスに設定します。newtype はあとで説明します。なお、ZipList は Show のインスタンスではないので、インタプリタ ghci で表示する場合は関数 getZipList を使って ZipList からリストを取り出してください。
簡単な実行例を示します。
Prelude Control.Applicative> getZipList $ ZipList [1,2,3,4] [1,2,3,4] Prelude Control.Applicative> getZipList $ (,) <$> ZipList "abc" <*> ZipList [1,2,3] [('a',1),('b',2),('c',3)] Prelude Control.Applicative> getZipList $ (,,) <$> ZipList "abc" <*> ZipList [1,2,3] <*> ZipList "def" [('a',1,'d'),('b',2,'e'),('c',3,'f')]
関数を ZipList に格納して、演算子 <*> で評価することもできます。
Prelude Control.Applicative> getZipList $ ZipList [(*2), (+10)] <*> ZipList [1,2] [2,12] Prelude Control.Applicative> 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 を使うとうまくいきます。
それでは実際に試してみましょう。
*Main> pure (*2) `app` Just 10 Just 20 *Main> pure (*2) `app` Nothing Nothing *Main> Nothing `app` Just 10 Nothing *Main> pure (*2) `app` Right 10 Right 20 *Main> pure (*2) `app` Left "error" Left "error" *Main> Left "error" `app` Right 10 Left "error" *Main> [(+3), (*2)] `app` [1,2,3,4,5] [4,5,6,7,8,2,4,6,8,10] *Main> [(+3), (*2)] `app` [] [] *Main> [] `app` [1,2,3,4,5] [] *Main> pure reverse `app` getLine hello, world "dlrow ,olleh"
もちろん、前回作成した型クラス Mfunctor の fmap' と組み合わせることもできます。
*Main> (*) `fmap'` Just 2 `app` Just 10 Just 20 *Main> (+) `fmap'` Right 2 `app` Right 10 Right 12 *Main> (^) `fmap'` [2,3] `app` [1..5] [2,4,8,16,32,3,9,27,81,243] *Main> (++) `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 は定義どおりにプログラムしただけです。そうはいっても、これだけでは何ができるのかよくわかりませんね。具体的な例を示しましょう。
*Main> :t pure' (+) pure' (+) :: (Mapplicative f, Num a) => f (a -> a -> a) *Main> :t pure' (+) `app` (+10) pure' (+) `app` (+10) :: Num a => a -> a -> a *Main> (pure' (+) `app` (+10)) 1 2 13 *Main> (pure' (+) `app` (*10)) 1 2 12
pure' (+) `app` (+10) は 2 つの数値を引数に取る関数になります。その動作は 1 を (+10) に適用して、その結果と 2 を足し算する、つまり 2 + (1+10) = 13 となります。(*10) の場合は 2 + (1*10) = 12 になります。
*Main> :t pure' (+) `app` (+10) `app` (*10) pure' (+) `app` (+10) `app` (*10) :: Num b => b -> b *Main> :t pure' (+) `app` (+10) `app` (*10) $ 10 pure' (+) `app` (+10) `app` (*10) $ 10 :: Num t => t *Main> pure' (+) `app` (+10) `app` (*10) $ 10 120
次に、`app` (*10) を追加しましょう。すると、合成された関数は引数が 1 つの関数になります。その動作は引数 10 を (+10) と (*10) に適用して、その結果を足し算する、つまり (10 + 10) + (10 * 10) = 120 になります。
pure' のかわりに fmap' を使っても同じように動作します。
*Main> (+) `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 で無限リストを生成しています。
簡単な実行例を示します。
*Main> getZipList $ pure' (*) `app` ZipList [1..5] `app` ZipList [11..15] [11,24,39,56,75] *Main> getZipList $ (*) `fmap'` ZipList [1..5] `app` ZipList [11..15] [11,24,39,56,75] *Main> getZipList $ ZipList [(+1), (+2), (+3)] `app` ZipList [10,11,12] [11,13,15] *Main> 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
プログラムは簡単ですね。実行例を示します。
*Main> liftA2 (+) (Just 1) (Just 2) Just 3 *Main> liftA2 (:) (Just 1) (Just [2]) Just [1,2] *Main> 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 を組み合わせてプログラムすることもできます。
簡単な実行例を示します。
*Main> sequenceA' [Just 1, Just 2, Just 3] Just [1,2,3] *Main> sequenceA' [Just 1, Just 2, Just 3, Nothing] Nothing *Main> sequenceA' [Right 1, Right 2, Right 3] Right [1,2,3] *Main> sequenceA' [Right 1, Right 2, Left "error", Right 3] Left "error" *Main> sequenceA' (map print [1,2,3,4,5]) 1 2 3 4 5 [(),(),(),(),()]
I/O アクションも Mapplicative なので、sequenceA' を使うとリストに格納された I/O アクションを実行することができます。
リストの場合、動作がもっと複雑になります。次の例を見てください。
*Main> sequenceA' [[1,2],[3,4]] [[1,3],[1,4],[2,3],[2,4]]
これは次のように動作します。
*Main> (:) `fmap'` [3,4] `app` [[]] [[3],[4]] *Main> (:) `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' はリストに格納された関数に引数を渡して評価することもできます。
*Main> :t sequenceA' [(+3), (*4), (^2)] sequenceA [(+3), (*4), (^2)] :: Num a => a -> [a] *Main> sequenceA' [(+3), (*4), (^2)] $ 5 [8,20,25]
Applicative にも満たすべき規則があります。まず大前提として、pure f <*> x = f <$> x を満たす必要があります。この規則を満たさないと Functor と Applicative を組み合わせて使うことができません。
それでは実際に試してみましょう。
*Main> pure' (+10) `app` Just 10 Just 20 *Main> (+10) `fmap'` Just 10 Just 20 *Main> pure' (+10) `app` Right 10 Right 20 *Main> (+10) `fmap'` Right 10 Right 20 *Main> pure' (+10) `app` [1,2,3,4,5] [11,12,13,14,15] *Main> (+10) `fmap'` [1,2,3,4,5] [11,12,13,14,15] *Main> getZipList $ (+10) `fmap'` ZipList [1,2,3,4,5] [11,12,13,14,15] *Main> 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 で詳しく説明されています。そちらをお読みくださいませ。
Prelude> pure' id `app` Just 10 Just 10 Prelude> pure' (.) `app` Just (*2) `app` Just (+5) `app` Just 10 Just 30 Prelude> Just (*2) `app` (Just (+5) `app` Just 10) Just 30 Prelude> pure' (*2) `app` Just 10 Just 20 Prelude> pure' ((*2) 10) :: Maybe Int Just 20 Prelude> Just (*2) `app` pure' 10 Just 20 Prelude> pure' ($ 10) `app` Just (*2) Just 20