あどけない話

インターネットに関する技術的な話など

SemigroupがMonoidに恋するとき

復習

Semigroup

class Semigroup a where
  (<>) :: a -> a -> a

Monoid

class Semigroup a => Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mappend = (<>)

本題

Haskellerの中には、「設定はMonoidであるべき」宗派が存在する。そのような信念を持つHaskellerが作ったライブラリを使おうとすると、Monoid のインスタンス(<>) でつないで設定データを構築することになる。

この記事では、SemigroupをMonoidに昇格させるのはいつかという話題を扱うので、まずSemigroupである設定データから始めよう。僕が最近秀逸だと思っているフラグを例として挙げる。

フラグには以下のような操作ができる:

  • フラグをセットする (FlagSet)
  • フラグをクリア(アンセット)する (FlagClear)
  • フラグをデフォルトに戻す (FlagReset)

これを実装するのは簡単だろう。(<>) の右が勝つことに決めれば、以下のようになる。

data FlagOp = FlagSet | FlagClear | FlagReset deriving (Eq,Show)

instance Semigroup FlagOp where
    _ <> op = op

使ってみよう:

> FlagSet <> FlagReset <> FlagClear
FlagClear

あとは、これを受け取って、

  • FlagSet ならフラグをセットする
  • FlagClear ならフラグをクリアする
  • FlagReset ならデフォルトの値を使用する

という関数を書くことになるだろう。そこは割愛する。

これで話が終われば、設定データは Semigroup で十分で Monoid にする必要はない。どうして、設定と言えば Monoid と言われるのだろう?

答えは「設定したい項目は複数あることが多い」からだ。FlagOpを複数格納しているデータをConfigとしよう。

data Config = Config { aflag :: FlagOp, bflag :: FlagOp } deriving (Eq,Show)

instance Semigroup Config where
    Config a1 b1 <> Config a2 b2 = Config (a1 <> a2) (b1 <> b2)

aflag をセットする setAFlag を作るとする。bflag は明らかに FlagSetFlagClear ではないので、FlagReset としてみる。

setAFlag :: Config
setAFlag = Config { aflag = FlagSet, bflag = FlagReset }

同様に、setBFlag も作る。

setBFlag :: Config
setBFlag = Config { aflag = FlagReset, bflag = FlagSet }

setAFlagsetBFlag を結合するとどうなるだろう?

> setAFlag <> setBFlag 
Config {aflag = FlagReset, bflag = FlagSet}

あれれれれ? aflagbflagFlagSet であるべきなのに、aflagFlagReset になってしまった。

どうやって直すべきだろうか? 元の値を保存する FlagKeep が必要そうだ。

元の値を保存する? それって、単位元では? ということは、Monoid に昇格するべきでは?

その通りだ。

data FlagOp = FlagSet | FlagClear | FlagReset | FlagKeep deriving (Eq,Show)

instance Semigroup FlagOp where
    op <> FlagKeep = op
    _ <> op = op

instance Monoid FlagOp where
    mempty = FlagKeep

data Config = Config { aflag :: FlagOp, bflag :: FlagOp } deriving (Eq,Show)

instance Semigroup Config where
    Config a1 b1 <> Config a2 b2 = Config (a1 <> a2) (b1 <> b2)

instance Monoid Config where
    mempty = Config mempty mempty

setAFlag :: Config
setAFlag = Config { aflag = FlagSet, bflag = mempty }

setBFlag :: Config
setBFlag = Config { aflag = mempty, bflag = FlagSet }

使ってみよう。

> setAFlag <> setBFlag 
Config {aflag = FlagSet, bflag = FlagSet}

めでたし、めでたし。

なお、僕は「設定はMonoidであるべき」宗派ではなく、「デフォルトの設定に対して変更関数を用意すべき」宗派なので、あしからず。