2009-09-17 [長年日記]

[Haskell] Maybeをfilterする

Maybeの中の値を元に、Maybe自体をNothingにしたいケースが結構あります。例えば、Maybe Stringで中の文字列が空の場合にはNothingにしたいケースなどです。

nonEmptyString :: Maybe String -> Maybe String
nonEmptyString s = case s of
                     Just s | s /= "" -> Just s
                     _                -> Nothing

これを少し一般化すると、こんな関数にできます。

filterMaybe :: (a -> Bool) -> Maybe a -> Maybe a
filterMaybe f (Just x) | f x = Just x
filterMaybe _ _              = Nothing

nonEmptyString = filterMaybe (/= "")

filterMaybeは、コンテナの中の値に応じてコンテナから必要な値だけを抜き出しているといえるので、リストに対するfilterと同じ動作をしていると考えられます。そこで、これらの型が両方とも属しているFoldableクラスを使って書けないか考えると、こんな形で書けそうです。

filter :: (Foldable f, Applicative t, Monoid (t a)) =>
          (a -> Bool) -> f a -> t a
filter f = foldMap (\x -> if f x then pure x else mempty)

畳み込むことができないとフィルタできないので、元のコンテナはFoldableである必要があります。出力側のコンテナは、中の値を持ち上げられなくてはいけないので、Applicativeにしてpureでコンテナの値に持ち上げます。さらに、フィルタの関数にマッチしなかった場合には、Monoidのmemptyで空のコンテナを生成します(いずれにしてもfoldMapの出力先のコンテナはMonoidである必要がありますけど)。

このようなfilterを用意すると、以下のように書くことができます。

nonEmptyString :: Maybe String -> Maybe String
nonEmptyString = filter (/= "")

ところが、MaybeのMonoidのインスタンスは、中の値がMonoidであることを要求し、mappendは中の値のmappendを呼び出すようになっています。このため、以下のようなことをすると予期しない結果になります。

x = filter (/= "") ["1", "2"] :: Maybe String

この結果は、Just "12"になってしまいます。また、以下のようにMonoidでない値を持つMaybeに対しては使えません。

data X = X | Y deriving (Show, Eq)
-- これはコンパイルできない
y = filter (/= X) $ Just X :: Maybe X

MaybeのMonoidインスタンスには、その他のバリエーションもあり、mappendでNothingでない最初の値を使うFirstと、逆に最後の値を使うLastが、Data.Monoidモジュールで定義されています。このインスタンスを使うと、上記の問題が解決できます。ただし、FirstやLastはFunctorでもApplicativeでもないので、これらのインスタンスにする必要があります。

{-# LANGUAGE StandaloneDeriving, GeneralizedNewtypeDeriving #-}
deriving instance Functor First
deriving instance Applicative First

これで、以下のように期待する値が取れるようになります。

x = getFirst $ filter (/= "") $ Just "1" -- Just "1"
y = getFirst $ filter (/= "") $ ["1", "2"] -- Just "1"
z = getFirst $ filter (/= X) $ Just Y -- Just Y

nonEmptyStringは、

nonEmptyString = getFirst . filter (/= "")

と書くことができます(これに関しては先のバージョンと結果は変わりませんけど)。

ちなみに、Data.Foldableのfindを使って、

nonEmptyString = find (/= "")

と書くのが最も手っ取り早いかもしれません。

[]

トップ «前の日記(2009-08-31) 最新