はじめに

Data.MonoidEndo 型が定義されています。

Endo という名前は 自己準同型 (Endomorphism) に由来します。

newtype Endo a = Endo { appEndo :: a -> a }

instance Semigroup a => Semigroup (Endo a) where
  Endo f <> Endo g = Endo (f . g)

instance Monoid (Endo a) where
  mempty = Endo id

使い方は簡単。

ghci> f = foldMap Endo [(+1), (*2), negate]
ghci> print $ appEndo f 5
-9

ghci> appEndo (Endo ("Hello, " ++) <> mempty <> Endo (++ "!")) "Haskell"
"Hello, Haskell!"

ghci> (appEndo $ foldMap Endo [("Hello, " ++), id, (++ "!")]) "Haskell"
"Hello, Haskell!"

appEndo すると関数が出てくるところがポイントですね。2つ目の評価の流れをざっくり追うとこんな感じです。

  appEndo (Endo ("Hello, " ++) <> Endo (++ "!")) "Haskell"
= appEndo (Endo (("Hello, " ++ ) . (++ "!"))) "Haskell"
= ("Hello, " ++ ) . (++ "!") $ "Haskell"
= "Hello, " ++ ("Haskell" ++ "!")
= "Hello, " ++ "Haskell!"
= "Hello, Haskell!"

Endo は意外と色んなところで使える便利なモノイドです。

Semigroup, Monoid law の確認

Semigroup Law

  Endo f <> (Endo g <> Endo h)
= Endo f <> Endo (g . h)
= Endo (f . (g . h))
-- Category の定義より f . (g . h) == (f . g) . h
= Endo ((f . g) . h)
= Endo (f . g) <> Endo h
= (Endo f <> Endo g) <> Endo h

Monoid Law

  Endo f <> (mempty :: Endo a)
= Endo f <> Endo id
= Endo (f . id)
-- Category の定義より f . id == f
= Endo f

  (mempty :: Endo a) <> Endo f
= Endo id <> Endo id
= Endo (id . f)
-- Category の定義より id . f == f
= Endo f

f . (g . h) == (f . g) . h

(.) の定義

(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \x -> f (g x)

証明

(LHS)
  f . (g . h)
= \x -> f ((g . h) x)
= \x -> f ((\y -> g (h y)) x)
= \x -> f (g (h x))

(RHS)
  (f . g) . h
= \x -> (f . g) (h x)
= \x -> (\y -> f (g y)) (h x)
= \x -> f (g (h x))

id . f == f

id の定義

id :: a -> a
id x = x

証明

  id . f
= \x -> id (f x)
= \x -> f x
= f

例1) Write Endo パターン

yesod-core パッケージの ProvideRep を扱う関数は Endo を利用しています。

selectRep :: MonadHandler m => Writer (Endo [ProvidedRep m]) () -> m TypedContent
provideRep :: (Monad m, HasContentType a) => m a -> Writer (Endo [ProvidedRep m]) ()
provideRepType :: (Monad m, ToContent a) => ContentType -> m a -> Writer (Endo [ProvidedRep m]) ()

また Yesod には GHState 型がありますが、そこでも Endo を使っています。

data GHState = GHState
    { ghsSession :: !SessionMap
    , ghsRBC     :: !(Maybe RequestBodyContents)
    , ghsIdent   :: !Int
    , ghsCache   :: !TypeMap
    , ghsCacheBy :: !KeyedTypeMap
    , ghsHeaders :: !(Endo [Header])
    }

このような WriterEndo を使った実装パターンは Quick and Easy DSLs with Writer Endo で紹介されている Writer Endo パターンとして知られているようです。

例2) データの更新

こんな感じで設定等を更新する際にも使えるかもしれません。

{-# LANGUAGE DataKinds        #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TypeOperators    #-}

import Control.Lens
import Data.Extensible
import Data.Monoid

type Person = Record
  '[ "name" >: String
   , "age"  >: Int
   ]

update :: [Person -> Person] -> Person -> Person
update fs = appEndo (foldMap Endo fs)

me :: [Person -> Person]
me =
  [ (& #name .~ "guchi")
  , (& #age .~ 20)
  ]

defaultPerson :: Person
defaultPerson = #name @= "NONAME"
             <: #age  @= 0
             <: nil

実行結果

λ> stack repl --package extensible --package lens EndoExample.hs

ghci> defaultPerson
name @= "NONAME" <: age @= 0 <: nil

ghci> update me defaultPerson
name @= "guchi" <: age @= 20 <: nil

例3) パターンマッチの実装

僕はあまり Endo モノイドを使いこなせていませんが、良い感じに使えたと思える例としては TAPL 11章でレコードパターンを実装する際です。

レコードのパターンマッチは代入の合成で書くことができるので、Endo がちょうどぴったり適用できました。

match :: Pattern -> Value -> (Term -> Term)
match (PtVar _ n) v = subst n v
match p@(PtRecord fs) v@(TmRecord fs')
  | isRecordValue v && sameFieldLength p v
      = appEndo $ foldMap (Endo . uncurry match) $ zip (map snd fs) (map snd fs')
  | otherwise = error "match: pattern match failure"
match PtRecord{} _ = error "match: v is not Rrcord"

参考