Home About
Haskell

Haskell / コンピュータリストに連続してフィルタする bind 使用編

前回 のコンピュータリストを使って、フィルタとモナドを試します。

Spread Sheet Computer List

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

computer.hs:

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

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
               ]

-- OSが Windows で 5万円より安いコンピュータを抽出する.
windowsComputerList = filter (\c -> os c == Windows) computerList
windowsAndLessThan50000ComputerList = filter (\c -> price c < 50000) windowsComputerList

GHCi を起動して OSが Windows で 5万円以下のコンピュータを表示:

$ ghci
> :load computer.hs
> windowsAndLessThan50000ComputerList
[Computer {name = "surface Go", os = Windows, price = 48000}]

うまくいきました。

ただ、この条件1(OSがWindows) かつ 条件2(5万円より安い) という2つの条件を一度に適用しようとすると:

windowsAndLessThan50000ComputerList = filter (\c -> price c < 50000) $ filter (\c -> os c == Windows) computerList

このように長くなってしまいます。 条件が2つ程度であれば問題ないのですが。

こんなときに使うのが Monad。 filterM というモナド版のfilter関数を使えば:

> windowsComputerListM = filterM (\c -> Just (os c == Windows)) computerList
> windowsComputerListM
Just [Computer {name = "surface laptop", os = Windows, price = 168000},Computer {name = "surface laptop Go", os = Windows, price = 68000},Computer {name = "surface pro", os = Windows, price = 198000},Computer {name = "surface Go", os = Windows, price = 48000},Computer {name = "thinkpad x1", os = Windows, price = 178000}]

filterM を使うには Control.Monad ( filterM ) を import しておく必要あり.

このように、filter と同じように コンピュータリストから OS が Windows のものを抽出できる。 ただし、filterM の最初の引数として適用する 関数 (\c -> Just (os c == Windows)) の戻り値が Bool ではなく Just Bool になっている点に注意。 そして、戻り値 windowsComputerListM は単なるリストではなく、 Just [Computer] になっている。

このままだと、最初の filter を使った場合と比べて、一見すると話がややこしくなっただけのように思えるが、実際はそうではなく次のコードのように:

-- 条件を用意:
isWindows     = filterM (\c -> Just (os c == Windows))
lessThan50000 = filterM (\c -> Just (price c < 50000))

-- 2つの条件をコンピュータリストに適用:
windowsAndLessThan50000ComputerListM = (Just computerList) >>= isWindows >>= lessThan50000

なんと >>= でつなぐことで連続して条件を適用できるという魔法っぽい処理が可能になる。

試しに isWindows のタイプを見る:

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

コンピュータのリストを適用して、Maybe で包んだコンピュータリスト(ここでは Windows のコンピュータリスト)を返すとなっている。

同様に lessThan50000 を見ると:

> lessThan50000 = filterM (\c -> Just (price c < 50000))
> :t lessThan50000
lessThan50000 :: [Computer] -> Maybe [Computer]

lessThan5000 もタイプは isWindows と同じです。

そして (Just computerList) のタイプは Maybe [Computer] です。

つまり、 >>= は (この例では) 左側がすべて Maybe [Computer] になっている。 言い換えると Maybe [Computer] そのものから出発、その後はそれ( Maybe [Computer] )を作り出す関数を >>= でつないで連続的に処理できることになる。

まとめると、以下のように >>= を使って連続して処理をつないでいけるということ。

Maybe [Computer] >>= ([Computer] -> Maybe [Computer]) >>= ([Computer] -> Maybe [Computer]) >>= ([Computer] -> Maybe [Computer]) 

それでは、条件をもっと追加して、この連続変換がほんとうに可能なのか実験してみよう。

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

macList50000to150000 = (Just computerList) >>= isMacOS >>= moreThan50000 >>= lessThan150000

GHCi で確認:

> :reload
> macList50000to150000
Just [Computer {name = "macbook air", os = MacOS, price = 98000}]

3つの条件(マックOSで 5万円〜15万円のコンピュータ)を満たしたコンピュータを抽出できています。

では、Windows と macOS で 5万円〜15万円の間の価格のコンピュータを抽出するには?

この場合、最初に Windows と macOS のコンピュータリストをつくる必要があります。 以下のように isWindowsOrMacOS を定義する方法もありえますが:

isWindowsOrMacOS = filterM (\c -> Just (os c == Windows || os c == MacOS))
windowsOrMacOSComputerList = (Just computerList) >>= isWindowsOrMacOS

isWindowsOrMacOS を定義しないで、 既にある isWindowsisMacOS を使って Windows と macOS のコンピュータリストをつくりだすことができます:

windowsOrMacOSComputerList = ((Just computerList) >>= isWindows) <> ((Just computerList) >>= isMacOS)

これは <> を使って左右を合成している。 その結果、Windows と macOS のコンピュータリストを生成している。

<> って何?という話ですが、 左右のタイプをそれぞれ調べると:

> :t (Just computerList) >>= isWindows
(Just computerList) >>= isWindows :: Maybe [Computer]
> :t (Just computerList) >>= isMacOS
(Just computerList) >>= isMacOS :: Maybe [Computer]

となっている。つまり、<> の左右は Maybe [Computer] になっている。 <> は Semigroup を合成するための関数で Maybe [Computer] が Semigroup でもあるので、合成することができる。

以上から WindowsとmacOSのコンピュータで 5〜15万円の間にあるコンピュータの抽出は:

windowsOrMacOSComputerList50000to150000 = windowsOrMacOSComputerList >>= isMacOS) >>= moreThan50000 >>= lessThan150000

GHCi で確認:

> windowsOrMacOSComputerList50000to150000
Just [Computer {name = "surface laptop Go", os = Windows, price = 68000},Computer {name = "macbook air", os = MacOS, price = 98000}]

うまくいきました。

コード全体 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

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 -> Just (os c == Windows))
isMacOS        = filterM (\c -> Just (os c == MacOS))
moreThan50000  = filterM (\c -> Just (price c > 50000))
lessThan150000 = filterM (\c -> Just (price c < 150000))

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

-- Windows と macOS のコンピュータリスト: 
windowsOrMacOSComputerList =( (Just computerList) >>= isWindows ) <> ( (Just computerList) >>= isMacOS )

-- Windows と macOS で 5..15万円のコンピュータリスト: 
windowsOrMacOSComputerList50000to150000 = windowsOrMacOSComputerList >>= moreThan50000 >>= lessThan150000

以上です。