Haskellのzip関数を一般化すると何になるか
この記事は Haskell Advent Calendar 2023 の7日目の記事です。
zip関数について
Haskellには zip 関数というものがあります。この関数は、2つのリストを受け取って、それぞれから取り出した要素を組にしたリストを返します。
zip :: [a] -> [b] -> [(a, b)]
実行例は次の通りです:
ghci> zip [1,2,3] ["Alpha","Bravo","Charlie"]
[(1,"Alpha"),(2,"Bravo"),(3,"Charlie")]
リストの要素の個数が異なる場合は、短い方に合わせられます。[1,2,3] を無限等差数列 [1..] に置き換えてみましょう:
ghci> zip [1..] ["Alpha","Bravo","Charlie"]
[(1,"Alpha"),(2,"Bravo"),(3,"Charlie")]
短い方(3要素)に合わせられました。
3つのリストを3要素タプルのリストに変換する zip3 関数や、4つ以上のリストに対して似たようなことをする関数もあります。
zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]
-- 以下は Data.List より
zip4 :: [a] -> [b] -> [c] -> [d] -> [(a, b, c, d)]
zip5 :: [a] -> [b] -> [c] -> [d] -> [e] -> [(a, b, c, d, e)]
zip6 :: [a] -> [b] -> [c] -> [d] -> [e] -> [f] -> [(a, b, c, d, e, f)]
zip7 :: [a] -> [b] -> [c] -> [d] -> [e] -> [f] -> [g] -> [(a, b, c, d, e, f, g)]
関連する関数として、zipWith というのもあります。これはタプルを作る代わりに、関数適用を行います。
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d]
-- Data.List には zipWith7 まである
zip を使うと zipWith f xs ys は map (uncurry f) (zip xs ys) と表現でき、逆に zipWith を使うと zip は zipWith (,) と表現できます。ですので、zip と zipWith は表現力的には同じものと思って良さそうです。
さて、Haskellのリストに関する関数は Foldable や Traversable に一般化できるものがちょいちょいあります。そして、zip 系の関数はリストだけではなく、Vector にも定義されています。zip 系の関数をリスト以外に一般化することはできないのでしょうか?言い換えると、以下が定義されるような f についての制約 ??? は何と呼ばれるべきでしょうか?
generalizedZip :: ??? f => f a -> f b -> f (a, b)
generalizedZipWith :: ??? f => (a -> b -> c) -> f a -> f b -> f c
満たすべき性質
何かを一般化するときは、何らかの法則を満たすように一般化したいです。zip が満たす性質はいくつかありますが、ここでは2点(+α)選びました。
まず、結合法則です。以下の2つは同型であって欲しいです:
zip xs (zip ys zs)
zip (zip xs ys) zs
実際の型は [(a, (b, c))] と [((a, b), c)] になるので同一ではないですが、内容としては同じです。
次に、交換法則です。以下の等式が成り立って欲しいです:
map (\(x,y) -> (y,x)) (zip xs ys) == zip ys xs
リストの zip に限れば、「zip xs (repeat ()) と xs が同型」みたいな性質もあります。Vector については repeat 関数がないのでこの性質もありません。
Applicativeクラス
上記の zip の性質を圏論的に言うと、対称モノイダル関手 (symmetric monoidal functor, 対称モノイド関手) となります。
モノイダル関手。どこかで聞いたことのある方もいるかもしれません。そう、Applicative関手の圏論的な呼び名が(強)laxモノイダル関手 ((strong) lax monoidal functor) でした。
圏論的な話をもっと知りたい方のために、私が昔書いた記事を挙げておきます:
- アプリカティブ関手ってなに?モノイド圏との関係は?調べてみました!(2018年12月)
余談ですが、昔の記事では私はmonoidal categoryの訳についてこんなことを書いていました:
モノイド圏 (monoidal category):文献によっては「モノイダル圏」と訳されることもある。しかし、“abelian group” は普通は「アーベリアン群」ではなく「アーベル群」と訳すし、“homological algebra” は「ホモロジカル代数」ではなく「ホモロジー代数」と訳す。よってこの文書では “monoidal category” は「モノイダル圏」ではなく「モノイド圏」と訳す。(1 文字短いし…)
が、monoidal categoryの訳は「モノイダル圏」で定着してしまったように見受けられます。なのでこの記事ではmonoidalの訳は「モノイダル」にしました。
閑話休題。
要するに、リスト型は zip によって Applicative のインスタンスとなるということです。実際、リスト型を zip によって Applicative のインスタンスとみなすnewtype wrapperが Control.Applicative で定義されています:
ghci> :m + Control.Applicative
ghci> (,) <$> ZipList [1,2,3] <*> ZipList ["Alpha","Bravo","Charlie"]
ZipList {getZipList = [(1,"Alpha"),(2,"Bravo"),(3,"Charlie")]}
リスト型のデフォルトの Applicative インスタンスは zip ではなく、モナドのインスタンスに基づいたものであることに注意してください。
ghci> (,) <$> [1,2,3] <*> ["Alpha","Bravo","Charlie"]
[(1,"Alpha"),(1,"Bravo"),(1,"Charlie"),(2,"Alpha"),(2,"Bravo"),(2,"Charlie"),(3,"Alpha"),(3,"Bravo"),(3,"Charlie")]
さて、Applicative クラスには
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
の一般化として
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
という関数が含まれています(ちなみに、GHC 9.6以降では liftA2 は Prelude からエクスポートされています)。よって、zip/zipWith の一般化は Applicative であると自信を持って言えるでしょう。……本当に?
Applicative クラスには pure :: a -> f a という関数が含まれています。リストの zip の場合(ZipList の場合)はこれは repeat :: a -> [a] です。ですが、Vector には repeat 関数はありません。なので、Vector を zip によって Applicative インスタンスとみなすことはできません(「長さが型レベル自然数で与えられた Vector 型」であれば Applicative のインスタンスにできます)。
結局、Vector も考慮した zip/zipWith の一般化は「Applicative クラスから pure を取り除いたもの」とするのが適切でしょう。何とも歯切れの悪い結論になりました。
ちなみに、Haskellには「Applicative クラスから pure を取り除いたもの」はありませんが、PureScriptにはそれが Applicative クラスのスーパークラスとして存在して、Apply クラスと呼ばれているようです。
おまけ:unzipの一般化
zip の逆として unzip という関数もあります。
unzip :: [(a, b)] -> ([a], [b])
これはリストの部分を Functor に一般化できます。
unzip :: Functor f => f (a, b) -> (f a, f b)
unzip x = (fmap fst x, fmap snd x)
そして、GHC 9.8ではこの Functor に関する unzip が Data.Functor からエクスポートされるようになりました。Data.Functor をunqualified importしつつ unzip を使っていた人は気をつけてください。対処法はqualified importするか、() でimportする名前を制限するか、となるでしょう。
Discussion