前回 コンピュータリストを bind ( >>= ) を使って連続して条件を適用しました。
しかし、コードをよく見てみると、 Just の部分を pure に変更しても作動するし、その逆に pure の部分を Just に変えても作動する。 今回はその謎を調べます。
名前+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]
できました。
今コンピュータリストから 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))
以上です。