Edited at

Monoid と DerivingVia


はじめに

突然ですが、みなさんは関数のモノイドの定義をご存知ですか?


クイズ

instance Monoid b => Monoid (a -> b) where

mempty = ...
(<>) = ...

すぐに思いつかない人はちょっと考えたらわかるので、頭の体操だと思って考えてみましょう!

今回は、この関数のモノイドの面白さについて書いてみたいと思います。モノイドといいつつ半群の話も含んでおります。

ヒント

Monoid.png


Monoid 型クラス

まずは Monoid 型クラスの定義について簡単に復習しておきましょう。


Monoid型クラスの定義

class Monoid a where

mempty :: a
(<>) :: a -> a -> a


Monoid則

-- 単位元 (左単位元、右単位元)

mempty <> a = a = a <> mempty

-- 結合律
a <> (b <> c) = (a <> b) <> c



具体例

> mempty :: String

""

> "has" <> "kell"
"haskell"

> mconcat ["has", "ke", "ll"]
"haskell"

> import Data.Monoid
> getSum $ Sum 5 <> Sum 10
15

> getProduct $ foldMap Product [5,10]
50


Monoid については、たくさんの良い文献があるのでこのぐらいにしておきます。


クイズの答え

考え方はこうです。


明示的に型を書いてみる

instance Monoid (a -> b) where

mempty :: a -> b
(<>) :: (a -> b) -> (a -> b) -> (a -> b)


型通りに少し考えてみる

instance Monoid (a -> b) where

mempty = \x -> ...
f <> g = \x -> ...

ここで登場人物の型を整理しておきましょう。


型の整理

f :: a -> b

g :: a -> b
x :: a

f <> g :: a -> b
(<>) f g x :: b
mempty x :: b


ここまできたら bMonoid になってくれていたら定義できそうだなって感じますね。


インスタンス定義完成

instance Monoid b => Monoid (a -> b) where

mempty _ = mempty
f <> g = \x -> f x <> g x

良くわかんないけどできた!


2引数関数はどうなるの?

確認してみましょう。つまり b(b -> c) になるんですね。


2引数のインスタンス定義(型)

instance Monoid (a -> (b -> c)) where

mempty :: a -> b -> c
(<>) :: (a -> b -> c) -> (a -> b -> c) -> (a -> b -> c)

いけそうな気がします。


2引数のインスタンス定義(クラス制約まだ)

instance Monoid (a -> (b -> c)) where

mempty _ _ = mempty
f <> g = \x y -> f x y <> g x y

Monoid クラス制約は最後の c だけで良さそうですね。


2引数のインスタンス定義(完成)

instance Monoid c => Monoid (a -> (b -> c)) where

mempty _ _ = mempty
f <> g = \x y -> f x y <> g x y

同様に任意の引数に対して動きます。


動かしてみよう!

定義はわかったけど、実際どうなのか良くわからないので動かしてみましょう。


具体例

> const "a" <> const "b" <> const "c" $ 1

"abc"

-- (1*10) + (1+10) + (1^10) = 22
> getSum $ (Sum.(*10) <> Sum.(+10) <> Sum.(^10)) $ 1
22

-- (1*10) * (1+10) * (1^10) = 110
$ (Product.(*10) <> Product.(+10) <> Product.(^10)) $ 1
110


面白い!とても面白い!!

使いづらいし良くわからないよって思った人は以下の図で覚えたら簡単です。

Monoid.png

実際には mconcat ではなく <> を使って右結合で畳み込まれます。

infixr 6 <>

f1 <> (f2 <> (f3 <> (f4 <> f5)))


DerivingVia と組み合わせると!

具体例としてユーザに文字列を Monoid で組み立ててもらい、後から指定した文字に対してエスケープをかけるような処理を考えてみましょう。(SQL で言うところの LIKEESCAPE のシチュエーションのような感じです)

{-# LANGUAGE OverloadedStrings #-}

{-# LANGUAGE DerivingVia #-}
module Lib where

import Data.String

-- Char はエスケープで利用する文字
newtype EscapedString = EscapedString { runEscape :: Char -> String }
deriving (Semigroup, Monoid) via (Char -> String)

instance IsString EscapedString where
fromString = preEscapedString

-- スマートコンストラクタ
preEscapedString :: String -> EscapedString
preEscapedString str = EscapedString (const str)

toEscapedString :: String -> EscapedString
toEscapedString str = EscapedString (escape '%' str)

escape :: Char -> String -> Char -> String
escape targetChar str c = foldr go [] str
where
go x xs
| targetChar == x = c:x:xs
| otherwise = x:xs

example :: EscapedString
example = "%haskell" <> userInput
where
userInput = toEscapedString "%"

> runEscape example '#'

"%haskell#%"

結構気に入った。


まとめ



  • Monad も良いけど Monoid も面白いんだよ。


  • DerivingVia も面白いんだよ。