あなたの知らないSemigroupの世界
自分で定義するデータの中には、足し算したくなるようなデータがある。たとえば、送信と受信のカウンターを定義したとしよう。
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のスーパークラスにするという野望があったからだ。
数学での定義を思い出そう:
状況把握
今後どのようなコードを書けばよいかという疑問に答えるためには、GHCの各バージョンでの状況を把握しなければならない。
GHC 7.10 (base 4.8)
GHC 7.10 では、みなさんご存知のように base パッケージに Data.Monoid モジュールがある:
-- base : Data.Monoid class Monoid a where mempty :: a mappend :: a -> a -> a (<>) :: a -> a -> a (<>) = mappend
Monoid型自体はPreludeの仲間入りを果たしたが、(<>)は明示的にimportする必要がある。
Data.Semigroupは、semigroupsパッケージで定義されている:
-- semigroup : Data.Semigroup 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パッケージへ移った:
-- base : Data.Monoid class Monoid a where mempty :: a mappend :: a -> a -> a (<>) :: a -> a -> a (<>) = mappend --base : Data.Semigroup 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)
何も変更なし。嵐の前の静けさだ。
対処方法
ここまで解説すれば、対処方法は明らかであろう。Semigroup (as superclass of) Monoid Proposalの最後に、semigroupsパッケージを使う方法と使わない方法が載っているので、よく眺めてほしい。