PR

zipWithN関数を一般化するZipList型

 ここまでは,Monadクラスのインスタンスでもある型の,Applicativeクラスに対するインスタンスを見てきました。実際には,Monadクラスのインスタンスではないが,FunctorクラスとApplicativeクラスのインスタンスではある型もあります。

 そうした型の例としては,Control.Applicativeモジュールで定義されているZipList型があります。ZipListは,liftMNやliftANと同様に,zipWith,zipWith3,zipWith4 ... といった一連のzipWithN関数を,Applicativeクラスのメソッドを使って一般化するための型です。

-- | Lists, but with an 'Applicative' functor based on zipping, so that
--
-- @f '<$>' 'ZipList' xs1 '<*>' ... '<*>' 'ZipList' xsn = 'ZipList' (zipWithn f xs1 ... xsn)@
--
newtype ZipList a = ZipList { getZipList :: [a] }

instance Functor ZipList where
    fmap f (ZipList xs) = ZipList (map f xs)

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

 ZipList型は,リストをZipListというデータ構成子に単純に包んだ型として定義されています。ZipListデータ構成子のgetZipListフィールドを使って,zipWithNという計算の最終結果を取り出します。

 Applicativeクラスに対するインスタンスでの<*>演算子の定義が,ZipListの肝になる部分です。この定義では,zipWith関数を使ってid関数をリストに適用しています。

Prelude> :t zipWith id
zipWith id :: [b -> c] -> [b] -> [c]

 この結果,zipWith関数は第1引数として,リストxsのそれぞれの要素に適用する「b -> c」型の関数のリストを要求することになります。渡すリストが「b -> c」のような1引数の関数のリストであれば,返り値が最終的な結果のリストになります。一方,渡すリストが「a -> b -> c」や「a -> b -> c -> d」のような2個以上の引数を取る関数のリストであれば,関数をリストxsのそれぞれの要素に対して適用した結果は,<*>演算子に渡すことが可能な部分適用された関数のリストになります。

 <$>演算子(fmapメソッド)では,map関数を使って関数fをfsに適用することで,<*>演算子に渡す関数のリストを作ります。pureメソッドでは,repeat関数を関数fに適用することで,そうしたリストを作ります。

 このように,ZipListでは「関数fを部分適用した関数のリストを作り,そのリストの要素である関数を,処理の対象であるリストのそれぞれの要素に適用する」という処理を繰り返すことで,zipWithN関数に相当する処理を行います。

Prelude Control.Applicative> getZipList $ (,) <$> ZipList [0..10] <*> ZipList [0..10]
[(0,0),(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10)]
Prelude Control.Applicative> getZipList $ (,,) <$> ZipList [0..10] <*> ZipList [0..10] <*> ZipList [0..10]
[(0,0,0),(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5),(6,6,6),(7,7,7),(8,8,8),(9,9,9),(10,10,10)]
Prelude Control.Applicative> getZipList $ pure (,) <*> ZipList [0..10] <*> ZipList [0..10]
[(0,0),(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10)]
Prelude Control.Applicative> getZipList $ pure (,,) <*> ZipList [0..10] <*> ZipList [0..10] <*> ZipList [0..10]
[(0,0,0),(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5),(6,6,6),(7,7,7),(8,8,8),(9,9,9),(10,10,10)]

 実際には,Data.Listモジュールではリストに対するzipWithN関数がzipWith7まで,vectorではVector型に対するzipWithN関数がzipWith6まで定義されています。このため,ZipList型が活躍する機会はそう多くはありません(参考リンク1参考リンク2)。

 しかし,定義済みの関数よりも多くの引数を取るzipWithN関数がどうしても必要になることがあるかもしれません。また,zipWithN関数が提供されていない型に対して,zipWithN関数が必要になる可能性もあります。そんなときには,ここで紹介したZipList型や,ZipList型に対してApplicativeクラスのインスタンスを定義するのに使った「関数の部分適用を使って<*>演算子を定義する」というテクニックを思い出してみてください。