Home About Contact
Haskell , Monad

Haskell / filterM を Maybe Bool だけに制限した filterMaybe 関数をつくる

前回 pure と Just の違い を調べていて、filterM の Maybe 限定版、というのを考えたのでそれを実際に試した。

Monad においては pure より return を使った方が普通なのかもしれません。わかりません。 Javaなどに慣れていると return は Java の return のイメージになり混乱するので、pure を使うで統一しています(今のところ)。 pure と return どっちなの?という件については https://wiki.haskell.org/Monad を見てください。

Spread Sheet Computer List

名前+OS+価格の一覧であるコンピュータリストがあったとして、これを使ってフィルタ処理してみます。

computer.hs:

import Control.Monad ( filterM )

type Name = String
type Price = Int

data OS = MacOS | IOS | ChromeOS | Windows deriving (Show, Eq)
data Computer = Computer { name :: Name
                         , os :: OS
                         , price :: Price }

instance Show Computer where
  show c = name c

computerList :: [Computer]
computerList = [ Computer "macbook air" MacOS 98000
               , Computer "macbook pro" MacOS 248000
               , Computer "iPad pro" IOS 128000
               , Computer "iPad air" IOS 68000
               , Computer "pixelbook" ChromeOS 158000
               , Computer "pixelbook Go" ChromeOS 78000
               , Computer "surface laptop" Windows 168000
               , Computer "surface laptop Go" Windows 68000
               , Computer "surface pro" Windows 198000
               , Computer "surface Go" Windows 48000
               , Computer "thinkpad x1" Windows 178000
               ]

filterMaybe :: (a -> Maybe Bool) -> [a] -> Maybe [a]
filterMaybe f list = filterM f list

ポイントは最後の2行です。

filterMaybe :: (a -> Maybe Bool) -> [a] -> Maybe [a]
filterMaybe f list = filterM f list

filterM のタイプは filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a] ですが、 filterMaybe は Applicative 制約 ではなく このように filterMaybe :: (a -> Maybe Bool) -> [a] -> Maybe [a] として Maybe に制約しています。

これで実験してみます。

GHCi を起動してまずは普通に isWindows 関数を定義:

$ ghci
> :load computer.hs
> isWindows = filterMaybe (\c -> Just (os c == Windows))

isWindows を使ってみます。

> (Just computerList) >>= isWindows 
Just [surface laptop,surface laptop Go,surface pro,surface Go,thinkpad x1]

うまくいきました。(まあ当然ですが)

では、isWindows の定義で Just ではなく pure を使ったらどうなるのか?

> isWindows = filterMaybe (\c -> pure (os c == Windows))
> (Just computerList) >>= isWindows
Just [surface laptop,surface laptop Go,surface pro,surface Go,thinkpad x1]
> (pure computerList) >>= isWindows
Just [surface laptop,surface laptop Go,surface pro,surface Go,thinkpad x1]

別に問題はないのですよ。ちょっと不思議なんですが。 filterMaybe は (a -> Maybe Bool) な関数を最初に適用する必要があるのだから (\c -> pure (os c == Windows)) で Just ではなく pure にしても通るのは不思議ですね。

しかも、(Just computerList)(pure computerList) にしても問題はありません。

これを想像してみると、isWindows の型シグネチャが (a -> Maybe Bool) と定義されているだから pure とコードしたとしても、Haskell がそれを Just したとして扱ってくれる(推論)ってことかな。

試しに filterMaybe の型シグネチャを filterM と同じにしてみれば...

--filterMaybe :: (a -> Maybe Bool) -> [a] -> Maybe [a]
filterMaybe :: Applicative m => (a -> m Bool) -> [a] -> m [a]
filterMaybe f list = filterM f list

isWindows = filterMaybe (\c -> pure (os c == Windows))

これで GHCi してみましょう:

> :reload
computer.hs:30:13: error:
    • Ambiguous type variable ‘m0’ arising from a use of ‘filterMaybe’
      prevents the constraint ‘(Applicative m0)’ from being solved.
      Relevant bindings include
        isWindows :: [Computer] -> m0 [Computer] (bound at computer.hs:30:1)
      Probable fix: use a type annotation to specify what ‘m0’ should be.
      These potential instances exist:
        instance Applicative IO -- Defined in ‘GHC.Base’
        instance Applicative Maybe -- Defined in ‘GHC.Base’
        instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’
        ...plus three others
        ...plus three instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the expression: filterMaybe (\ c -> pure (os c == Windows))
      In an equation for ‘isWindows’:
          isWindows = filterMaybe (\ c -> pure (os c == Windows))
   |
30 | isWindows = filterMaybe (\c -> pure (os c == Windows))

おっと、エラーです。 潜在的に 以下の3つの可能性があるがどれになるのか決定できない!

      These potential instances exist:
        instance Applicative IO -- Defined in ‘GHC.Base’
        instance Applicative Maybe -- Defined in ‘GHC.Base’
        instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’

といったメッセージです。

pure しているけど、これがどのインスタンスになるのか決定できない、と。 では、Haskell がこれが Maybe であると決定できるようにコードを足せばいいのかな?

windowsComputerList = (Just computerList) >>= isWindows

これで GHCi で reload してみます。

> :reload
[1 of 1] Compiling Main             ( computer.hs, interpreted )
Ok, one module loaded.

OKです!

ならば逆に... Just ではなく pure にしたらどうなるのか?

windowsComputerList = (pure computerList) >>= isWindows

これで GHCi で reload してみます。

> :reload
エラー

やはりNGです。先程と同じようなエラーが出てコンパイルできません。

ならば、filterMaybe の型シグネチャを元に戻した上で... つまり Maybe に制約した上で、 (pure computerList) >>= isWindows したら、それはエラーにならないのだろうか? 試してみましょう。

filterMaybe :: (a -> Maybe Bool) -> [a] -> Maybe [a]
--filterMaybe :: Applicative m => (a -> m Bool) -> [a] -> m [a]
filterMaybe f list = filterM f list

isWindows = filterMaybe (\c -> pure (os c == Windows))
windowsComputerList = (pure computerList) >>= isWindows

これで GHCi で reload してみます。

> :reload
[1 of 1] Compiling Main             ( computer.hs, interpreted )
Ok, one module loaded.

OKです!

つまり、今回は filterMaybe の型シグネチャで Maybe を使うことが定義されているので、 コード中に Just と書かないで pure と書いても、そこが Maybe であると (Haskell の方で) 推論して決定できるということでしょう。(たぶん)

まとめ

Haskell が推論してそれを一意に決定できる限りは、 pure を使うほうがよい ということらしい。

最終的に作動した、filterMaybe を使った、できるだけ pure なコード computer.hs を書き留めておきます。

import Control.Monad ( filterM )

type Name = String
type Price = Int

data OS = MacOS | IOS | ChromeOS | Windows deriving (Show, Eq)
data Computer = Computer { name :: Name , os :: OS , price :: Price }

instance Show Computer where
  show c = name c

computerList :: [Computer]
computerList = [ Computer "macbook air" MacOS 98000
               , Computer "macbook pro" MacOS 248000
               , Computer "iPad pro" IOS 128000
               , Computer "iPad air" IOS 68000
               , Computer "pixelbook" ChromeOS 158000
               , Computer "pixelbook Go" ChromeOS 78000
               , Computer "surface laptop" Windows 168000
               , Computer "surface laptop Go" Windows 68000
               , Computer "surface pro" Windows 198000
               , Computer "surface Go" Windows 48000
               , Computer "thinkpad x1" Windows 178000
               ]

filterMaybe :: (a -> Maybe Bool) -> [a] -> Maybe [a]
filterMaybe f list = filterM f list

isWindows = filterMaybe (\c -> pure (os c == Windows))
windowsComputerList = (pure computerList) >>= isWindows

(最終的に実行するときまでには) pure なままではいられない... けれども、できるだけ pure な方がいい、ということらしい。

追伸

だから、今回は実験なので上記のようにコードしたが、普通は以下のようにコードするのであろう。

--filterMaybe :: (a -> Maybe Bool) -> [a] -> Maybe [a]
--filterMaybe f list = filterM f list

isWindows = filterM (\c -> pure (os c == Windows))
windowsComputerList = (Just computerList) >>= isWindows

以上です。