自分で定義するデータの中には、足し算したくなるようなデータがある。たとえば、送信と受信のカウンターを定義したとしよう。
data Metrics = Metrics {
rx :: Int
, ts :: Int
} deriving (Eq, Show)
これは以下のように足し算できると嬉しいだろう。
> Metrics 1 2 + Metrics 3 4
Metrics {rx = 4, ts = 6}
しかしこれは Num のインスタンスにすべきではない。このデータ型に掛け算は定義できないからだ。かといって、addMetrics みたいな関数を定義するのはかっこ悪い。
> Metrics 1 2 `addMetrics` Metrics 3 4
Metrics {rx = 4, ts = 6}
このように演算子が一個だけ欲しいと思ったら、それは多分 Monoid だ。
import Data.Monoid
instance Monoid Metrics where
mempty = Metrics 0 0
Metrics r1 t1 `mappend` Metrics r2 t2 = Metrics (r1 + r2) (t1 + t2)
GHC 7.10までは、(<>) が mappend の別名であるので、以下のようなコードが書ける。
> Metrics 1 2 <> Metrics 3 4
Metrics {rx = 4, ts = 6}
やったね!
GHC 8.4へようこそ
上記のコードを GHC 8.4 で読み込むと以下のようなエラーが出る。
Example.hs:8:10: error:
・ No instance for (Semigroup Metrics)
arising from the superclasses of an instance declaration
・In the instance declaration for ‘Monoid Metrics’
|
8 | instance Monoid Metrics where
| ^^^^^^^^^^^^^^
これはどういうことだろう? その疑問に答えるのがこの記事の主旨である。
mappendよりも(<>)の方がかっこいいのに、長い間 (<>) はMonoidのメソッドにはしてもらえなかった。あくまで別名であった。それは一部の人に、SemigroupをMonoidのスーパークラスにするという野望があったからだ。
数学での定義を思い出そう:
半群 (Semigroup)
- 結合則: (a <> b) <> c = a <> (b <> c)
モノイド (Monoid)
- 結合則: (a <> b) <> c = a <> (b <> c)
- 単位元:e <> a = a <> e = a
群 (Group)
- 結合則: (a <> b) <> c = a <> (b <> c)
- 単位元:e <> a = a <> e = a
- 逆元:a <> inv a = e
さっきの疑問に答えると、GHC 8.4ではSemigroupがMonoidのスーパークラスとなり、Metricsに対する(<>)の定義がないために、エラーが出たという訳だ。
状況把握
今後どのようなコードを書けばよいかという疑問に答えるためには、GHCの各バージョンでの状況を把握しなければならない。
GHC 7.10 (base 4.8)
GHC 7.10 では、みなさんご存知のように base パッケージに Data.Monoid モジュールがある:
class Monoid a where
mempty :: a
mappend :: a -> a -> a
(<>) :: a -> a -> a
(<>) = mappend
Monoid型自体はPreludeの仲間入りを果たしたが、(<>)は明示的にimportする必要がある。
Data.Semigroupは、semigroupsパッケージで定義されている:
class Semigroup a where
(<>) :: a -> a -> a
default (<>) :: Monoid a => a -> a -> a
(<>) = mappend
最後の default は、DefaultSignatures という拡張で、Monoidの制約を持てば Semigroupの方の (<>) は mappend で代用できると読む。親子関係がひっくり返っていて、なんだかなぁという感じである。
GHC 8.0 (base 4.9)
Data.Semigroupがsemigroupパッケージからbaseパッケージへ移った:
class Monoid a where
mempty :: a
mappend :: a -> a -> a
(<>) :: a -> a -> a
(<>) = mappend
class Semigroup a where
(<>) :: a -> a -> a
親子関係はない。
フラグ -Wnoncanonical-monoid-instances が定義された。これは、MonoidのインスタンスなのにSemigroupのインスタンスになってないと警告を出すフラグである。デフォルトは off。上位互換性に関するフラグ -Wcompat を付けても、警告が出る。
まだ GHC 8.4 を使えない人は、-Wall の横に -Wcompat を書き足して遊んでみるとよい。
GHC 8.2 (base 4.10)
何も変更なし。嵐の前の静けさだ。
GHC 8.4 (base 4.11)
なんとなんと、MonoidとSemigroupがPreludeの仲間に入った。そして、SemigroupがMonoidのスーパークラスとなった。
class Semigroup a where
(<>) :: a -> a -> a
class Semigroup a => Monoid a where
mempty :: a
訂正:SemigroupがMonoidのスパークラスになったために、(<>) を定義してないとエラーが出るようになった。嵐がやってきたのだ。