Home About
Haskell

Haskell / コンピュータリストを連続してフィルタする(リファクタリング)

前回 コンピュータリストを bind ( >>= ) を使って連続して条件を適用しました。

しかし、コードをよく見てみると、 Just の部分を pure に変更しても作動するし、その逆に pure の部分を Just に変えても作動する。 今回はその謎を調べます。

Spread Sheet Computer List

Step1 Just か pure か

名前+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 } --deriving Show

-- 表示はコンピュータ名だけにする:
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
               ]

GHCi を起動して computer.hs を load します:

$ ghci
> :load computer.hs

まず OS が Windows という条件を定義し、タイプを確認:

> isWindows = filterM (\c -> Just (os c == Windows))
> :t isWindows
isWindows :: [Computer] -> Maybe [Computer]

コンピュータのリストを受け取り Maybe で包んだ コンピュータリストを戻す関数です。

これを使ってみます:

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

うまく行きました。結果として Just ウインドウズのコンピュータリスト が戻ってきています。

これと同じことを今度は bind >>= を使って:

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

同じ結果になります。bind の左側が Just [Computer] で 右側(isWindows) が [Computer] -> Maybe [Computer] です。

これを Just ではなく pure にしたらどうなるのか?

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

先程と異なり戻り値に Just が表示されていません。

まずそもそも isWindows のタイプは Just から pure に変えたことでどうなったのか?

> :t isWindows
> isWindows :: Applicative m => [Computer] -> m [Computer]

コンピュータのリストを適用し、Applicative 型で包まれた コンピュータリストを戻すように変わっている。

これを使って変換した結果のコンピュータリストのタイプはどうなっているのか?

> windowsComputerList = isWindows computerList
> :t windowsComputerList
windowsComputerList :: Applicative m => m [Computer]

Just [Computer] ではなく Applicative m の型制約のある m [Computer] になっています。 なるほど、 Just (つまり Maybe 型) も Applicative の一種なので別にそれでもいいのだが、 pure を使うことで Maybe だけでなく Applicative な型クラスなら、Maybe に限らずどれでもいい、という定義に変わった。

そもそも filterM のタイプを見てみると:

> :t filterM
filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a]

となっていて、最初に適用するのは Applicative m の型制約の付いた (a -> m Bool) 関数となっている。 もし (a -> Maybe Bool) のように定義されていたとしたら、 (\c -> Just (os c == Windows)) と書く必要があるが、そうではない。 ここは Maybeに限定されているわけではなく Applicative であればよい。

この辺 (a -> Maybe Bool) にしたらどうなるのかこのエントリーで検証した。

ということで、ここは (\c -> pure (os c == Windows)) と書いて問題ない。 というかその方が抽象度が高い。 Java ならばメソッドの引数部分の定義に 具体的なクラスではなく インタフェースを書くイメージと見た。

ならば、前回 のコードを pure を使って書き直してみます。

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
               ]

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

-- Windowsのリスト:
winList = (pure computerList) >>= isWindows 

これを GHCi で reload:

> :reload
たくさんのエラー

全体が Applicative 型で、具体的な型を類推できないから駄目、みたいな感じ。 やっぱり pure なままじゃ駄目 ってことかな。

このエントリーで検証した。必ずしも pure なままじゃ駄目でもなかった。

では (Just computerList) してみよう。

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

GHCiで確認:

> :reload
> winList
Just [surface laptop,surface laptop Go,surface pro,surface Go,thinkpad x1]

うまくいきました。

GHCi上で定義して使っていたときは pure なままでも、このエラーでなかった気がするのだが...

では、Just を必要箇所に使用して前回のコードを書き直したのがこれ:

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
               ]

isWindows      = filterM (\c -> pure (os c == Windows))
isMacOS        = filterM (\c -> pure (os c == MacOS))
moreThan50000  = filterM (\c -> pure (price c > 50000))
lessThan150000 = filterM (\c -> pure (price c < 150000))

-- マックOSで 5..15万円のコンピュータリスト:
macList50000to150000 = (Just computerList) >>= isMacOS >>= moreThan50000 >>= lessThan150000

-- Windows と macOS をあわせたコンピュータリスト: 
windowsAndMacComputerList =( (Just computerList) >>= isWindows ) <> ( (Just computerList) >>= isMacOS )

-- Win or Mac で 5..15万円の間にあるコンピュータリスト:
windowsOrMacOSComputerList50000to150000 = windowsAndMacComputerList >>= moreThan50000 >>= lessThan150000

GHCi で確認:

> :reload
> windowsOrMacOSComputerList50000to150000
Just [surface laptop Go,macbook air]

できました。

Step2 もっと柔軟に

今コンピュータリストから OS または 価格を使って フィルタすることで自分のほしいコンピュータを抽出する、 ということをやっているわけだが、現状それらの条件はこの4つの関数:

を使っている。

でも 20万円以内でもいいか、とか。やっぱり 10万円以下の ChromeOS にするわ、とか。 気持ちがうつろうと、次々に条件関数を定義する必要が生じる。 それはやだ。

では汎用的な関数をつくろう。こんなふうに:

isAnyOS :: Applicative m => OS -> [Computer] -> m [Computer]
isAnyOS x = filterM (\c -> pure (os c == x))

これならば isAnyOS ChromeOS のように書けば ChromeOS のフィルタに isAnyOS MacOS と書けば、MacOS のフィルタになってくれる。 価格用も用意しよう。

lessThan :: Applicative m => Price -> [Computer] -> m [Computer]
lessThan x = filterM (\c -> pure (price c < x))

moreThan :: Applicative m => Price -> [Computer] -> m [Computer]
moreThan x = filterM (\c -> pure (price c > x))

これで、たとえば、6万円から15万円の間のコンピュータを抽出するには:

(Just computerList) >>= (moreThan 60000) >>= (lessThan 150000)

でいけるはず。

GHCi で確認:

> :reload
> (Just computerList) >>= (moreThan 60000) >>= (lessThan 150000)
Just [macbook air,iPad pro,iPad air,pixelbook Go,surface laptop Go]

できた。isAnyOS 関数も使おう:

> (Just computerList) >>= (moreThan 60000) >>= (lessThan 150000) >>= (isAnyOS Windows)
Just [surface laptop Go]

この価格帯で購入できる Windows は surface laptop Go だと。

最後にリファクタリングした 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
               ]


isAnyOS :: Applicative m => OS -> [Computer] -> m [Computer]
isAnyOS x = filterM (\c -> pure (os c == x))

lessThan :: Applicative m => Price -> [Computer] -> m [Computer]
lessThan x = filterM (\c -> pure (price c < x))

moreThan :: Applicative m => Price -> [Computer] -> m [Computer]
moreThan x = filterM (\c -> pure (price c > x))

以上です。