Моноид Maybe
Рассмотрим несколько способов, которыми для типа Maybe a могут быть определены экземпляры класса Monoid, и обсудим, чем эти экземпляры полезны.
Один из способов состоит в том, чтобы обрабатывать тип Maybe a как моноид, только если его параметр типа a тоже является моноидом, а потом реализовать функцию mappend так, чтобы она использовала операцию mappend для значений, обёрнутых в конструктор Just. Мы используем значение Nothing как единичное, и поэтому если одно из двух значений, которые мы объединяем с помощью функции mappend, равно Nothing, мы оставляем другое значение. Вот объявление экземпляра:
instance Monoid a => Monoid (Maybe a) where
mempty = Nothing
Nothing `mappend` m = m
m `mappend` Nothing = m
Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)
Обратите внимание на ограничение класса. Оно говорит, что тип Maybe является моноидом, только если для типа a определён экземпляр класса Monoid. Если мы объединяем нечто со значением Nothing, используя функцию mappend, результатом является это нечто. Если мы объединяем два значения Just с помощью функции mappend, то содержимое значений Just объединяется с помощью этой функции, а затем оборачивается обратно в конструктор Just. Мы можем делать это, поскольку ограничение класса гарантирует, что тип значения, которое находится внутри Just, имеет экземпляр класса Monoid.
ghci> Nothing `mappend` Just "андрей"
Just "андрей"
ghci> Just LT `mappend` Nothing
Just LT
ghci> Just (Sum 3) `mappend` Just (Sum 4)
Just (Sum {getSum = 7})
Это полезно, когда мы имеем дело с моноидами как с результатами вычислений, которые могли окончиться неуспешно. Из-за наличия этого экземпляра нам не нужно проверять, окончились ли вычисления неуспешно, определяя, вернули они значение Nothing или Just; мы можем просто продолжить обрабатывать их как обычные моноиды.
Но что если тип содержимого типа Maybe не имеет экземпляра класса Monoid? Обратите внимание: в предыдущем объявлении экземпляра единственный случай, когда мы должны полагаться на то, что содержимые являются моноидами, – это когда оба параметра функции mappend обёрнуты в конструктор Just. Когда мы не знаем, являются ли содержимые моноидами, мы не можем использовать функцию mappend между ними; так что же нам делать? Ну, единственное, что мы можем сделать, – это отвергнуть второе значение и оставить первое. Для этой цели существует тип First a. Вот его определение:
newtype First a = First { getFirst :: Maybe a }
deriving (Eq, Ord, Read, Show)
Мы берём тип Maybe a и оборачиваем его с помощью декларации newtype. Экземпляр класса Monoid в данном случае выглядит следующим образом:
instance Monoid (Firsta) where
mempty = First Nothing
First (Just x) `mappend` _ = First (Just x)
First Nothing `mappend` x = x
Значение mempty – это просто Nothing, обёрнутое с помощью конструктора First. Если первый параметр функции mappend является значением Just, мы игнорируем второй. Если первый параметр – Nothing, тогда мы возвращаем второй параметр в качестве результата независимо от того, является ли он Just или Nothing:
ghci> getFirst $ First (Just 'a') `mappend` First (Just 'b')
Just 'a'
ghci> getFirst $ First Nothing `mappend` First (Just 'b')
Just 'b'
ghci> getFirst $ First (Just 'a') `mappend` First Nothing
Just 'a'
Тип First полезен, когда у нас есть множество значений типа Maybe и мы хотим знать, является ли какое-либо из них значением Just. Для этого годится функция mconcat:
ghci> getFirst . mconcat . map First $ [Nothing, Just 9, Just 10]
Just 9
Если нам нужен моноид на значениях Maybe a – такой, чтобы оставался второй параметр, когда оба параметра функции mappend являются значениями Just, то модуль Data.Monoid предоставляет тип Last a, который работает, как и тип First a, но при объединении с помощью функции mappend и использовании функции mconcat сохраняется последнее значение, не являющееся Nothing:
ghci> getLast . mconcat . map Last $ [Nothing, Just 9, Just 10]
Just 10
ghci> getLast $ Last (Just "один") `mappend` Last (Just "два")
Just "two"