fugafuga.write

日々のログ

すごいH本 part80

リストはモノイド

インスタンス宣言

instance Monoid [a] where
    mempty = []
    mappend = (++)

リストは中身の型がなんであっても常に Monoid のインスタンスにできる。

*Main> [1,2,3] `mappend` [4,5,6]
[1,2,3,4,5,6]
*Main> ("one" `mappend` "two") `mappend` "tree"
"onetwotree"
*Main> "one" `mappend` ("two" `mappend` "tree")
"onetwotree"
*Main> "one" `mappend` "two" `mappend` "tree"
"onetwotree"
*Main> "pang" `mappend` mempty
"pang"
*Main> mconcat [[1,2],[3,6],[9]]
[1,2,3,6,9]
*Main> mempty :: [a]
[]

Product と Sum

数をモノイドにする方法として、 * を二項演算にして 1 を単位元とする方法の他に、 + を二項演算にして 0 を単位元とする方法がある。

*Main> 0 + 4
4
*Main> 5 + 0
5
*Main> (1 + 3) + 5
9
*Main> 1 + (3 + 5)
9

足し算もモノイド則が成り立つ。

*+ ともにモノイド則を満たしているが、 ある型に対して同じ型クラスのインスタンスを複数定義したい場合、 newtype に包んで新しい型をインスタンスにする方法がとれる。

Data.Monoid モジュールはこのために ProductSum という型をエクスポートしている。

newtype Product a = Product { getProduct :: a }
    deriving (Eq, Ord, Read, Show, Bounded)

Product の Monoid インスタンスはこのようになる

instance Num a => Monoid (Product a) where
    mempty = Product 1
    Product x `mappend` Product y = Product (x * y)

すでに Num のインスタンスであるようなすべての型 a について、 Product a は Monoid のインスタンスになる。

*Main Data.Monoid> getProduct $ Product 3 `mappend` Product 9
27
*Main Data.Monoid> getProduct $ Product 3 `mappend` mempty
3
*Main Data.Monoid> getProduct $ Product 3 `mappend` Product 4 `mappend` Product 2
24
*Main Data.Monoid> getProduct . mconcat . map Product $ [3,4,2]
24

Sum も同様

*Main Data.Monoid> getSum $ Sum 2 `mappend` Sum 9
11
*Main Data.Monoid> getSum $ mempty `mappend` Sum 3
3
*Main Data.Monoid> getSum . mconcat . map Sum $ [1,2,3]
6

Any と All

Num a と同じようにモノイドにする方法が2通りあるものは他にもある。

Bool|| をモノイド演算として、False を単位元とする方法。 || は論理和を表し、2つの引数のいずれかが True なら True を返す。

newtypeAny が定義されている。

newtype Any = Any { getAny :: Bool }
    deriving (Eq, Ord, Read, Show, Bounded)

インスタンス定義

instance Monoid Any where
    mempty = Any False
    Any x `mappend` Any y = Any (x || y)

Any と呼ばれるのは、

x `mappend` y

が、x か y のいずれか(any)が True であった場合に、True になるから。

*Main Data.Monoid> getAny $ Any True `mappend` Any False
True
*Main Data.Monoid> getAny $ mempty `mappend` Any True
True
*Main Data.Monoid> getAny . mconcat . map Any $ [False, False, False, True]
True
*Main Data.Monoid> getAny $ mempty `mappend` mempty
False

もう1つの方法は、 && をモノイド演算として、True を単位元とする方法。

newtype All = All { getAll :: Bool }
    deriving (Eq, Ord, Read, Show, Bounded)

インスタンス定義

instance Monoid All where
    mempty = All True
    All x `mappend` All y = All (x && y)

確認

*Main Data.Monoid> getAll $ mempty `mappend` All True
True
*Main Data.Monoid> getAll $ mempty `mappend` All False
False
*Main Data.Monoid> getAll . mconcat . map All $ [True, True, True]
True
*Main Data.Monoid> getAll . mconcat . map All $ [True, True, False]
False

2つの引数がともに True である場合に限り True を返す。

Ordering モノイド

ものを比較した結果を返すのに使う。 LT, EQ, GT という3つの値のいずれかをとる。

インスタンス宣言はこう

instance Monoid Ordering where
    mempty = EQ
    LT `mappend` _ = LT
    EQ `mappend` y = y
    GT `mappend` _ = GT

文字列を辞書順で比較するときのルールに合わせた定義になっている。 EQが単位元になっているのは、2つの単語の同じ位置に同じ文字を挿入しても辞書順は変化しないから。

Ordering の Monoid インスタンスでは

x `mappend` y

は、

y `mappend` x

と一致しない。左辺がEQで無い限り左辺が優先されるため。

2つの文字列を引数にとり、その長さを比較して Ordering を返す関数を書く。

lengthCompare :: String -> String -> Ordering
lengthCompare x y = let a = length x `compare` length y
                        b = x `compare` y
                    in if a == EQ then b else a

Monoid を使って書く

import Data.Monoid

lengthCompare :: String -> String -> Ordering
lengthCompare x y = (length x `compare` length y) `mappend`
                    (x `compare` y  )

実行

*Main Data.Monoid> lengthCompare "zen" "ants"
LT
*Main Data.Monoid> lengthCompare "zen" "ant"
GT

単語の中から母音の数も比較し、それを2番めに重要な条件にする。

import Data.Monoid

lengthCompare :: String -> String -> Ordering
lengthCompare x y = (length x `compare` length y) `mappend`
                    (vowels x `compare` vowels y) `mappend`
                    (x `compare` y  )
    where vowels = length . filter (`elem` "aeiou")

実行

*Main Data.Monoid> lengthCompare "zen" "anna"
LT
*Main Data.Monoid> lengthCompare "zen" "ana"
LT
*Main Data.Monoid> lengthCompare "zen" "ann"
GT

Maybe モノイド

Maybe a も複数の方法でモノイドになれる。

1つ目の方法。型引数 a がモノイドであるときに限り Maybe a もモノイドであるとし、 Maybe a の mappend を、Just の中身の mappend を使って定義する。Nothing を単位元とする。

instance Monoid a => Monoid (Maybe a) where
    mempty = Nothing
    Nothing `mappend` m = m
    m `mappend` Nothing = m
    Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)

使う

*Main Data.Monoid> Nothing `mappend` Just "andy"
Just "andy"
*Main Data.Monoid> Just LT `mappend` Nothing
Just LT
*Main Data.Monoid> Just (Sum 3) `mappend` Just (Sum 4)
Just (Sum {getSum = 7})

失敗する可能性のある計算の返り値をモノイドとして扱いたい場合に便利。

Maybe の中身が Monoid インスタンスでなかった場合、mappend は使えない。 そこで First a という型を使う。 値に Maybe a が強制される。

newtype First a = First { getFirst :: Maybe a }
    deriving (Eq, Ord, Read, Show)

インスタンス宣言

instance Monoid (First a) where
    mempty = First Nothing
    First (Just x) `mappend` _ = First (Just x)
    First Nothing `mappend` x = x

実行

*Main Data.Monoid> getFirst $ First (Just 'a') `mappend` First (Just 'b')
Just 'a'
*Main Data.Monoid> getFirst $ First Nothing `mappend` First (Just 'b')
Just 'b'
*Main Data.Monoid> getFirst $ First (Just 'a')  `mappend` First Nothing
Just 'a'
*Main Data.Monoid> getFirst . mconcat . map First $ [Nothing, Just 9, Just 10]
Just 9

First は、いくつもある Maybe 値にどれか1つでも Just があるか調べたい時に便利。

Last もある。

*Main Data.Monoid> getLast . mconcat . map Last $ [Nothing, Just 9, Just 10]
Just 10
*Main Data.Monoid> getLast $ Last (Just "one") `mappend` Last (Just "two")
Just "two"

Nothing でない最後の値が採用される。

所感

Ordering と Maybe の2項演算子については+, *と違って具体的な演算子が無い?のでわかりにくい。

あと、First が出てきたところで、

1つの選択は、第一引数を返して第二引数は捨てる、と決めておくことです。

この文章の意味が理解できていない。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る