Haskell
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 も面白いんだよ。