背景
すごいHaskellたのしく学ぼう!を以前読んだのですが、モノイドやモナドあたりから理解が曖昧でした。
最近モノイドについて復習しましたので、備忘録としてまとめておこうと思います。
Monoidの実装
HaskellのMonoidクラスは以下のような定義です。
昔はMonoid単体で実装されていました。しかし、GHC8.4からはSemigroupを用いた定義に変更されました。
実装が必要なのはSemigroupの (<>)
と Monoidのmempty
で、そのほかの関数はデフォルト実装があります。つまりSemigroup + 単位元
の実装でMonoidを作れます。
class Semigroup a where
(<>) :: a -> a -> a
class Semigroup a => Monoid a where
mempty :: a
mappend :: a -> a -> a
mappend = (<>)
mconcat :: [a] -> a
mconcat = foldr mappend mempty
モノイド則
しかしMonoidクラスの実装だけでは、モノイドの条件を十分に満たせません。モノイドが別途満たすべき法則は以下の3つです。満たさなくてもエラーは出ませんが、これらの法則を守ることが期待されます。
mempty <> x = x
x <> mempty = x
(x <> y) <> z = x <> (y <> z)
注意点
掛け算を例に挙げられていることがありますが、a * b = b * a
を満たす必要はありません。あくまで1つ目と2つ目の法則は単位元に対する計算ルールであり、単位元以外の値同士の計算は左右が入れ替わっても問題ありません。
Monoidの使い方
今回は家族のショッピングカートをマージするという例で考えます。
カートと商品の定義
ショッピングカートと、カートに入れる商品は以下のように定義してあります。
単に結合する操作の実装だけならSemigroupで十分です。しかし単位元があると、データ型に付随した初期化方法で統一できるため、コードが書きやすくなります。
-- 商品データ型
data Item = Item {
itemName :: String, -- 商品名
itemPrice :: Double -- 価格
} deriving (Show)
-- ショッピングカートデータ型
data ShoppingCart = ShoppingCart {
items :: [Item] -- 商品のリスト
} deriving (Show)
-- ショッピングカートをセミグループにし、カート同士をマージできるようにする
instance Semigroup ShoppingCart where
(ShoppingCart items1) <> (ShoppingCart items2) = ShoppingCart (items1 ++ items2)
-- ショッピングカートをモノイドにし、空のカートを定義
instance Monoid ShoppingCart where
mempty = ShoppingCart []
mappend = (<>)
家族の商品カートをマージする例
これらのデータ型を使って家族のカートを<>
でマージします。マージには<>
を使っていますが、let familyCart = mconcat [cartA, cartB, cartC]
でも可能です。
main :: IO ()
main = do
-- 家族の各メンバーが追加する商品(cartCは空カート)
let cartA = ShoppingCart [Item "Milk" 2.50, Item "Bread" 1.20]
cartB = ShoppingCart [Item "Apple" 0.99, Item "Banana" 1.29]
cartC = mempty :: ShoppingCart
-- 全員のカートをマージ
-- let familyCart = mconcat [cartAlice, cartBob, cartCharlie]
let familyCart = cartA <> cartB <> cartC
putStrLn $ "Family Cart: " ++ (show familyCart)
実行結果
それぞれ家族のカートをマージすることができました。
Family Cart: ShoppingCart {items = [Item {itemName = "Milk", itemPrice = 2.5},Item {itemName = "Bread", itemPrice = 1.2},Item {itemName = "Apple", itemPrice = 0.99},Item {itemName = "Banana", itemPrice = 1.29}]}
モノイドを使わないver(余談)
ショッピングカートをモノイドにしないと以下のようになります。
モノイドを使用すると、<>
演算子が二つのショッピングカートを結合するためのものであることが直感的に理解できます。しかし、モノイドを使わない場合は、どんな引数を取るのかや関数の動作を確認する手間が増え、可読性が下がります。
さらに、モノイドを使わない方法では、空のショッピングカートを作る関数を別に定義する必要があり、コードを統一するのに不便です。例えば、emptyCart
関数を別途定義しなければならず、コードが冗長になります。モノイドを使用することで、mempty
として空のカートを一貫して利用できるため、コードがより簡潔になります。
module Main (main) where
-- 商品データ型
data Item = Item {
itemName :: String,
itemPrice :: Double
} deriving (Show)
-- ショッピングカートデータ型
data ShoppingCart = ShoppingCart {
items :: [Item]
} deriving (Show)
-- 空のショッピングカートを作る関数
emptyCart :: ShoppingCart
emptyCart = ShoppingCart []
-- ショッピングカート同士をマージする関数
mergeCarts :: ShoppingCart -> ShoppingCart -> ShoppingCart
mergeCarts (ShoppingCart items1) (ShoppingCart items2) = ShoppingCart (items1 ++ items2)
main :: IO ()
main = do
-- 家族の各メンバーが追加する商品(cartCは空カート)
let cartA = ShoppingCart [Item "Milk" 2.50, Item "Bread" 1.20]
cartB = ShoppingCart [Item "Apple" 0.99, Item "Banana" 1.29]
cartC = emptyCart
-- 全員のカートをマージ
let familyCart = mergeCarts cartA (mergeCarts cartB cartC)
putStrLn $ "Family Cart: " ++ show familyCart
まとめ
モノイドを使うことで、他の開発者が見た際の可読性を上げられるのがいいですね。
ただ、私はモノイドなものを見つけるのが苦手なので、世の中にあるモノイドを見つける努力をしようと思います。
近々モナドについても学習しようと思いますが、正直モノイドで苦戦しているので大変そうです。