Magical Monoids
Recently something caught my eye while hacking on a Haskell
project. Pay attention to the mempty
in the code fragment below:
readTextFileUtf8 :: FilePath -> IO (Either Utf8Error T.Text)
readTextFileUtf8 filename = {- ... -}-- | Return "" in case of errors
lenientReadTextFileUtf8 :: FilePath -> IO T.Text
lenientReadTextFileUtf8 filename = do
result <- readTextFileUtf8 filename
pure $ either (\_ -> mempty) id result
I went about my favourite pastime did some code-golfing on lenientReadTextFileUtf8
but inadvertently made a mistake while refactoring which didn’t result in a type error:
lenientReadTextFileUtf8' :: FilePath -> IO T.Text
lenientReadTextFileUtf8' filename
= either mempty pure =<< readTextFileUtf8 filename
Can you spot the “mistake”?
I should have written
... = either (\_ -> pure mempty) pure =<< ...
as in my mind I expected mempty
to be (“” :: Text)
and to my surprise the code actually even worked in the way I wanted it to!
So it was time to go deeper and investigate. After reading up on the base
documentation I quickly found out that there’s two peculiar instances(1) defined for Monoid
which enable this type magic:
instance Monoid b => Monoid (a -> b) where
mempty = \_ -> memptyinstance Monoid a => Monoid (IO a) where
mempty = pure mempty
In other words, mempty
can be specialised into an infinite number of functions according to the repeated application of the definitions above:
mempty :: IO Text
mempty :: IO (IO (IO Text))
mempty :: IO (a -> IO (b -> Text))
mempty :: a -> Text
mempty :: a -> b -> Text
mempty :: a -> b -> IO Text
mempty :: a -> b -> IO (IO Text)
So whenever you’re too lazy to write nested forms of const
or pure
you can just let the Haskell compiler magically generate those forms on the fly for you via the magic of type inference!
But there’s more! Check out the associated Semigroup
instances
instance Semigroup b => Semigroup (a -> b) where
f <> g = \x -> f x <> g x
instance Semigroup a => Semigroup (IO a) where
(<>) = liftA2 (<>)
With those you can now write stuff like
>>> (take 3 <> const "oi" <> drop 4) "Monads are cool!"
"Monoids are cool!"
or
readTextFileUtf8OrLatin1 :: FilePath -> IO (Either String T.Text)
readTextFileUtf8OrLatin1 = readTextFileUtf8 <> readTextFileLatin1
which tries to read a file in UTF8 format and if that fails falls back to reading as Latin1. Aren’t Monoid
s super awesome? Do you know of other cool Monoid
magical tricks?
Footnotes:
(1) Wouldn’t it be even cooler if this worked for every Applicative
?
instance (Applicative f, Semigroup a) => Semigroup (f a) where
(<>) = liftA2 (<>)instance (Applicative f, Monoid a) => Monoid (f a) where
mempty = pure mempty
Update: On Reddit phadej and others pointed out there’s the Ap
newtype wrapper defined in the latest base
release which allows just that! Its definition is
newtype Ap f a = Ap { getAp :: f a }instance (Applicative f, Semigroup a) => Semigroup (Ap f a) where
(Ap x) <> (Ap y) = Ap $ liftA2 (<>) x yinstance (Applicative f, Monoid a) => Monoid (Ap f a) where
mempty = Ap $ pure mempty