\gdef\Set{\mathrm{\mathbf{Set}}} \gdef\Hask{\mathrm{\mathbf{Hask}}} \gdef\Vect{\mathrm{\mathbf{Vect}}} \gdef\Mon{\mathrm{\mathbf{Mon}}} \gdef\id{\mathrm{id}} \gdef\Id{\mathrm{Id}} \gdef\map{\mathrm{map}} \gdef\op#1{{#1}^{\mathrm{\scriptsize op}}} \gdef\listof#1{\left\lbrack{#1}\right\rbrack}
まえがき
先日、Witherable という型クラスについての理論的な面からの紹介を、Twitterにて投稿しました1。 これをちゃんと記事にします。
Filterable
とWitherable
witherable
パッケージでは、Filterable
とWitherable
という二つの型クラスが提供されています。
この2つのクラスはいったい何で、どういったことができるのか見ていきます。
Filterable
の概要
-- Witherable.hsより抜粋
class Functor f => Filterable f where
mapMaybe :: (a -> Maybe b) -> f a -> f b
= catMaybes . fmap f
mapMaybe f
catMaybes :: f (Maybe a) -> f a
= mapMaybe id
catMaybes
filter :: (a -> Bool) -> f a -> f a
filter f = mapMaybe $ \a -> if f a then Just a else Nothing
-- mapMaybe か catMaybes のどちらか一方が必須
{-# MINIMAL mapMaybe | catMaybes #-}
Filterable
は、その名のとおりfilter
関数filter :: (a -> Bool) -> [a] -> [a]
や、
filter
より少し強力な機能を持つmapMaybe :: (a -> Maybe b) -> [a] -> [b]
を、
リスト以外のコレクション型に一般化する型クラスです。
リストのmapMaybe
がmap
の代わりにもfilter
の代わりにもなることを反映して、
Filterable
なコレクション型はすべてFunctor
でもあることが求められています。
(つまり、Filterable
の親クラスとしてFunctor
があります。)
まずはFilterable
のインスタンスにはどのようなものがあるのかを見てみましょう。
頻繁に使うコレクション型には、すでに(別の名前空間の下で)mapMaybe
と名のつく関数があるので、
Filterable
はその関数を使うだけです。
import qualified Data.Vector as V
import qualified Data.Map as M
instance Filterable [] where
= Data.Maybe.mapMaybe
mapMaybe
instance Filterable Vector where
= V.mapMaybe
mapMaybe
instance Filterable (Map k) where
= M.mapMaybe mapMaybe
また、一見コレクション型には見えないものにもmapMaybe
が適用できることがあります。
import Data.Functor.Const(Const(..))
-- | MaybeもFilterableになれる。「長さが0か1のリスト」とみなした場合に一致
instance Filterable Maybe where
-- Filterable MaybeはMaybeモナドを使って定義できます!
mapMaybe :: (a -> Maybe b) -> Maybe a -> Maybe b
= (=<<)
mapMaybe
catMaybes :: Maybe (Maybe a) -> Maybe a
= join
catMaybes
-- | (Const c)も「要素がゼロ個しか入らないコンテナ」としてFilterableになれる
instance Filterable (Const c) where
mapMaybe :: (a -> Maybe b) -> Const c a -> Const c b
Const c) = Const c mapMaybe _ (
Witherable
の概要
Witherable
は、言うなれば「副作用のあるmapMaybe
」をするためのクラスで、
そのためのメソッドwither
を持っています。以下に定義を示します。
class (Traversable t, Filterable t) => Witherable t where
wither :: Applicative f => (a -> f (Maybe b)) -> t a -> f (t b)
-- 必須でないメソッドをいくつか省略
例えば、次のような場合にwither
が使えます。
-- | 50%の確率で、ランダムに Just a か Nothing のどちらかを返す
coinToss :: a -> IO (Maybe a)
-- | 50%の確率でランダムに要素を削除する
randomDrop :: Witherable t => t a -> IO (t a)
= wither coinToss randomDrop
これは、Traversable
クラスがFunctor
クラスに対して、
「副作用のあるfmap
をする」とでも言うべき2traverse
メソッドを持っていることに似ています。
Functor
、Traversable
、Filterable
とWitherable
の関係を図にすると以下のようになります。
さて、Witherable
が何をするクラスなのかを説明しましたが、
ここである疑問が湧いてきた方も居るかもしれません。
Witherable
は本当に必要なのか?
なぜかというと、あらゆるTraversable
かつFilterable
なコレクション型に対して使える、
汎用のwither
の定義があるからです。
witherDefault :: (Traversable t, Filterable t, Applicative f)
=> (a -> f (Maybe b)) -> t a -> f (t b)
= fmap catMaybes . traverse f witherDefault f
もちろんHaskellのプログラムとしての効率(速度・メモリ使用)
の面からいって、各コレクション型に特化したwither
を用いる意味があるため、
Witherable
クラスの必要性は否定できません。しかし、
効率を無視して純粋に何ができるかだけ考えたときに、Witherable
というクラスを設ける必要性はあるのでしょうか?
しかも、Witherable
クラスには上記のwitherDefault
がデフォルトの実装として採用されており、
手動で書かないといけない関数はひとつもありません。ますます疑念がわいてきます。
ですが、よくよく調べてみると、
Witherable
はTraversable + Filterable
とまったく同じではないことが分かります。
witherDefault
は、どんなTraversable
かつFilterable
な型t
に対しても使えますが、
「Witherable
のwither
」には、Witherable
則によって追加の保証がなされています。
つまり、wither
が満たすべき性質を、witherDefault
では保証できないことがあるのです。
次の節から、具体的にはどういうことなのか説明します。
Traversable
のおさらい
準備として、Traversable
クラスと、そのTraversable
則についておさらいしておきます。
class (Functor t, Foldable t) => Traversable t where
{-# MINIMAL traverse | sequenceA #-}
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f
sequenceA :: Applicative f => t (f a) -> f (t a)
sequenceA = traverse id
Traversable
は、traverse
またはsequenceA
のどちらかを与えることで定義できます。
どちらを基準にしてもよいのですが、ここではsequenceA
の方で定義されているとしましょう。
sequenceA
は「t
とf
の合成関手の間の自然変換」という型を持つため、以下のようにストリング図3で表現しやすいのです。
図を見やすくするため、この記事を通して、
ストリング図中ではsequenceA
のことをギリシャ文字デルタ(\delta)一文字で表記することにします。
また、本来は小文字で書いていたt
やf
といった関手の名前も、大文字でTのように書いてしまいます。
Traversable
則を定義するやりかたはいくつかありますが、
標準ライブラリのドキュメントData.Traversable
における定義がそのままで便利です。
- Naturality
-
全てのアプリカティブ準同型
t
に対してt . sequenceA = sequenceA . fmap t
- Identity
-
sequenceA . fmap Identity = Identity
-
Composition
-
sequenceA . fmap Compose = Compose . fmap sequenceA . sequenceA
ここで、アプリカティブ準同型とは、以下の関数
t :: (Applicative f, Applicative g) => f a -> g a
で、アプリカティブの演算を保つものです。つまり以下の等式が成り立ちます。
pure x) = pure x
t (<*> x) = t f <*> t x t (f
また、恒等関手Identity
と関手の合成Compose
はそれぞれ
Data.Functor.Identityと
Data.Functor.Composeです。
これをストリング図に描いたものが以下になります。
Filterable
の理論
Filterable
は「mapMaybe
に似ている関数」をまとめたものでした。
しかし、(a -> Maybe b) -> t a -> t b
という型をした関数を何でもmapMaybe
に当てはめていいとすると、
それが本当にリストとの類推ができるような操作なのかは分かりません。
そこで、Functor
則やApplicative
則のように、Filterable
則が決められています。
- Conservation(保存則)
-
mapMaybe (Just . f) = fmap f
- Composition(合成則)
-
mapMaybe f . mapMaybe g = mapMaybe (f <=< g)
ストリング図への描きやすさから、Filterable
則をmapMaybe
の代わりにcatMaybes
で表現し直しておきます。
まず、mapMaybe
とcatMaybes
は互いに定義可能でした。
-- (再掲)
class Functor f => Filterable f where
mapMaybe :: (a -> Maybe b) -> f a -> f b
= catMaybes . fmap f
mapMaybe f
catMaybes :: f (Maybe a) -> f a
= mapMaybe id catMaybes
catMaybes
で表現したFilterable
則は以下のようになります。
これが実際にmapMaybe
での表現と同値であることは各自お確かめ下さい。
- Identity(単位則)
-
catMaybes . fmap Just = id
- Composition(合成則)
-
ここでcatMaybes . catMaybes = catMaybes . fmap join
join
はMaybe
のMonad
演算join :: Maybe (Maybe a) -> Maybe a
これをストリング図に書くと以下のようになります。
ただし、sequenceA
を描くときに\delta一文字に略したように、
ストリング図中では次の記法を使うことにします。
Maybe
はPと略します。catMaybes :: t (Maybe a) -> t a
は、3本の線(上にPとT、下にT)が出ている○で表します。
脱法Filterable
Filterable
則の定式化は単純で理論的にもきれいです4。しかし、
Filterable
という名前からは少し意外な振る舞いをするインスタンスも存在します。
例えば、次のU
型は、0個か2個の要素を持つコンテナ型で、
2個の要素のうち片方がNothing
になると、もう片方も”連帯責任”で削除されてしまいます。
data U a = U0 | U2 a a
deriving (Show, Eq, Functor, Foldable, Traversable)
-- DeriveTraversable拡張を使用
instance Filterable U where
catMaybes :: U (Maybe a) -> U a
U2 (Just x) (Just y)) = U2 x y
catMaybes (= U0 catMaybes _
このような変なふるまいをするcatMaybes
でも、Filterable
則を満たすれっきとしたインスタンスです。
Identity
catMaybes . fmap Just = id
ua :: U a
としたとき、catMaybes . fmap Just $ ua = ua
を確認する。-- ua = U0 の場合 . fmap Just $ U0 catMaybes = catMaybes U0 = U0 -- ua = U2 x y の場合 . fmap Just $ U2 x y catMaybes = catMaybes $ U2 (Just x) (Just y) = U2 x y
Composition
catMaybes . catMaybes = catMaybes . fmap join
umma :: U (Maybe (Maybe a))
としたとき、catMaybes (catMaybes umma) = catMaybes (fmap join umma)
を確認する。-- umma = U0 の場合 . catMaybes $ U0 catMaybes = catMaybes U0 = U0 . fmap join $ U0 catMaybes = catMaybes U0 = U0 -- umma = U2 mmx mmy -- mmx = Just mx, mmy = Just my の場合 . catMaybes $ U2 (Just mx) (Just my) catMaybes = catMaybes $ U2 mx my . fmap join $ U2 (Just mx) (Just my) catMaybes = catMaybes $ U2 (join (Just mx)) (join (Just my)) = catMaybes $ U2 mx my -- mmx, mmyのどちらかがNothingの場合 . catMaybes $ U2 mmx mmy catMaybes = catMaybes U0 = U0 . fmap join $ U2 mmx mmy catMaybes = catMaybes $ U2 (join mmx) (join mmy) {- join mmx, join mmyのどちらかはNothingなので -} = U0
また違った振る舞いのインスタンスも定義できます。
今度は、2要素のうち片方だけがNothing
になると、もう片方の要素をコピーして2要素を保ちます。
data V a = V0 | V2 a a
deriving (Show, Eq, Functor, Foldable, Traversable)
-- DeriveTraversable拡張を使用
instance Filterable V where
catMaybes :: V (Maybe a) -> V a
V0 = V0
catMaybes V2 Nothing Nothing) = Zero
catMaybes (V2 Nothing (Just y)) = V2 y y
catMaybes (V2 (Just x) Nothing) = V2 x x
catMaybes (V2 (Just x) (Just y)) = V2 x y catMaybes (
Identity
catMaybes . fmap Just = id
va :: V a
としたとき、catMaybes . fmap Just $ va = va
を確認する。-- va = V0 の場合 . fmap Just $ V0 catMaybes = catMaybes V0 = V0 -- va = V2 x y の場合 . fmap Just $ V2 x y catMaybes = catMaybes $ V2 (Just x) (Just y) = V2 x y
Composition
catMaybes . catMaybes = catMaybes . fmap join
vmma :: V (Maybe (Maybe a))
としたとき、catMaybes (catMaybes vmma) = catMaybes (fmap join vmma)
を確認する。-- vmma = V0 の場合 . catMaybes $ V0 catMaybes = catMaybes V0 = V0 . fmap join $ V0 catMaybes = catMaybes V0 = V0 -- vmma = V2 Nothing Nothing の場合 . catMaybes $ V2 Nothing Nothing catMaybes = catMaybes V0 = V0 . fmap join $ V2 Nothing Nothing catMaybes = catMaybes $ V2 Nothing Nothing = V0 -- vmma = V2 (Just mx) (Just my) の場合 . catMaybes $ V2 (Just mx) (Just my) catMaybes = catMaybes $ V2 mx my . fmap join $ V2 (Just mx) (Just my) catMaybes = catMaybes $ V2 mx my -- vmma = V2 (Just mx) Nothing の場合 . catMaybes $ V2 (Just mx) Nothing catMaybes = catMaybes $ V2 mx mx . fmap join $ V2 (Just mx) Nothing catMaybes = catMaybes $ V2 mx Nothing -- ここで mx で場合分けする -- Nothing -> 両辺ともV0 -- Just x -> 両辺ともV2 x x -- vmma = V2 Nothing (Just my) の場合も -- xとyを入れ替えただけで同様
こうした奇妙なインスタンスが禁止されるような、より厳しい法則は考えられないでしょうか?
実は、Filterable
だけを使ってもうまくいきません5。
そのため、Foldable
やTraversable
の機能をも使って、厳しい法則を定めてみましょう。
上記のU
やV
がcatMaybes
として「ふさわしくない感じがする」理由の一つとして、
存在する要素の個数がcatMaybes
によって変わってしまうという点がありそうです。
この「要素の個数」というのはFoldable
の機能です。「要素の個数が変わらない」を定式化してみましょう6。
-- Filterableのより強い法則(候補1):
. catMaybes = countJusts
length' where
-- Int の代わりに Sum Int を返す length
length' :: (Foldable t) => t a -> Sum Int
= Sum . length
length' = foldMap (const 1)
countJusts :: (Foldable t) => t (Maybe a) -> Sum Int
= foldMap length' -- このlength' は Maybe a -> Sum Int
countJusts -- (Justなら1, Nothingなら0)
-- 補助的な関数を使わずに書くなら:
foldMap (const 1) . catMaybes = foldMap (foldMap (const 1))
U
もV
もこの法則候補1を満たすことができませんね!
もう少し強い要請として、「要素だけをリストに取り出したもの」がcatMaybes
によって変わらない、というものも考えられます。
-- Filterableのより強い法則(候補2):
. catMaybes = concatMap toList :: t (Maybe a) -> [a]
toList -- concatMapもtoListもfoldMapの特別な場合です。
-- concatMap = foldMap :: (Foldable t) => (a -> [b]) -> t a -> [b]
-- toList = foldMap singleton
-- where singleton a = [a]
-- 代入して foldMap だけで表現すると以下のようになります。
foldMap singleton . catMaybes = foldMap (foldMap singleton)
これらを統合して、foldMap
がcatMaybes
で変わらない、としてもよいでしょう。
-- Filterableのより強い法則(候補3):
foldMap f . catMaybes = foldMap (foldMap f)
今考えているFilterable
なコンテナ型はFunctor
でもありました。
ここではさらにFoldable
でもあるものを考えています。
Functor
かつFoldable
ならば、
その両方を親クラスに持つTraversable
であってもおかしくないでしょう7。
foldMap
がtraverse
/sequenceA
で実装できることを思い出すと、
(候補3)のTraversable
版は以下のようになります。
-- Filterableのより強い法則:
traverse f . catMaybes = fmap catMaybes . traverse (traverse f)
-- sequenceAで表現するならば:
sequenceA . catMaybes = fmap catMaybes . sequenceA . fmap sequenceA
この法則にはDistributivityという名前を付けることにします。
Distributivityから(候補1)〜(候補3)が導けるので、
U
やV
はFilterable
かつTraversable
でありながら、
Distributivityを満たさないような具体例となっています。
絵も描いておきます。
ただし、これまでの略記法
Maybe
をPと書くcatMaybes
を○で表すsequenceA
を\deltaと書く
をフル活用しています。\delta_PはMaybe
に対するsequenceA
のことです。
Witherable
の理論
この記事のはじめのほうでWitherable
の定義と使い方を説明し、「Witherable
はFilterable
とTraversable
の組み合わせに過ぎないのではないか?」という疑問を取り上げました。
繰り返しになりますが、実はWitherable
はFilterable
+Traversable
より厳しい制限が課せられているのです。この制限はWitherable
則として表されています。
Witherable
の定義を再掲します。
class (Traversable t, Filterable t) => Witherable t where
wither :: Applicative f => (a -> f (Maybe b)) -> t a -> f (t b)
Witherable
則はwither
を使って記述されていますが、
今後の事情のために(traverse
に対してsequenceA
を使ったように)以下の
\omegaという関数を代わりに使うことにします。
\omegaからwither
を、逆にwither
から\omegaを定義できるので、どちらを使ってもいいはずです。
:: (Witherable t, Applicative f) => t (f (Maybe a)) -> f (t a)
ω= wither id
ω wither :: (Witherable t, Applicative f) => (a -> f (Maybe b)) -> t a -> f (t b)
= ω . fmap f wither f
これをストリング図に描くと以下のようになります。
\omegaを使ったWitherable
則は以下のようになります。
- Naturality(自然性)
-
全てのアプリカティブ準同型
t
に対して. ω = ω . fmap t t
- Identity(単位則)
-
. fmap Identity . fmap Just = Identity ω
- Composition(合成則)
-
Compose . fmap ω . ω = ω . fmap (Compose . fmap ω_P)
ただし、\omega_Pは
Maybe
に対する\omegaで、 以下のように定義される。
Witherable は Filterable+Traversable ではない
Witherable
とその法則は上記のように定義されています。
このとき、Witherable t
はFilterable t
かつTraversable t
かつt
がDistributivityを満たすことと同値になることが証明できます。
(Distributivityが本当に追加の条件であってFilterable
則やTraversable
則から導けないことは、上記のU
やV
が存在することからわかります。)
次のcatMaybesDefault
によって、
Witherable
はFilterable
の機能を併せ持つ上位互換であることがわかります。
catMaybesDefault :: Witherable t => t (Maybe a) -> t a
= runIdentity . ω . fmap Identity catMaybesDefault
Filterable
則もWitherable
則から証明できます。
Identity(単位則)
Composition(合成則)
同じようにWitherable
はTraversable
の機能も併せ持ち、
Traversable
則はWitherable
則から証明できます。
sequenceADefault :: (Witherable t, Applicative f) => t (f a) -> f (t a)
= ω . fmap (fmap Just) sequenceADefault
Naturality(自然性)
Identity(単位則)
Composition(合成則)
さらに、catMaybesDefault
とsequenceADefault
の2つから、
ωを復元できます。
ω = fmap catMaybes . sequenceA
そして最後に、Witherable
から取り出したFilterable
とTraversable
は、
Distributivity――先程定義した、「変な」Filterable
を除外する法則――を満たすことが証明できます。
証明
つまり、Witherable
は少なくともFilterable
かつTraversable
かつDistributivityをもつような型クラスです。
逆に、Filterable
かつTraversable
かつDistibutivityをもつならば、witherDefault
がWitherable
則を満たすことも示せ、結果として同値性が分かります。
証明
結論
Witherable
はFilterable
とTraversable
の両方の機能をもち、この2クラスに「脱法Filterable
封じのDistributivity
則」を合わせたものと同値で、
それをwither
と3つのWitherable
則で表したものでした!
人によっては、副作用のある
fmap
と言ったら mapMの方を思い出すかもしれません。 実のところtraverse
とmapM
は兄弟みたいなものです。↩︎fmap :: (a -> b) -> t a -> t b traverse :: Applicative f => (a -> f b) -> t a -> f (t b) mapM :: Monad m => (a -> m b) -> t a -> m (t b)
Filterable f
は、簡潔に「f
はKleisli Maybe
から->
への関手である」 と表現されることもあり、Filterable
則は関手の従うべき単位則と合成則を言い直したものに相当します。 今回の記事とはあまり関係しない論点ですが、過去の投稿で何度か触れております: 型クラスAlignについて 随伴から作られるMonad(準備編)↩︎某所で議論があり、「うまくいかない」というのは技術的に微妙なポイントなのですが、 あまり整理されていないかつ口論が混じっている会話なのでリンクはしません↩︎
Sum
は Data.Monoid にあります↩︎Foldable
には法則がないので、可能ならTraversable
を使いたいのです↩︎