Как легко использовать моноидные и комбинировать значения с пользовательской операцией?

То, что я пытаюсь сделать, тривиально определить вручную, в основном

maybeCombine :: (a->a->a) -> Maybe a -> Maybe a -> Maybe a
maybeCombine _ Nothing Nothing = Nothing
maybeCombine _ (Just a) Nothing = Just a
maybeCombine _ Nothing (Just a) = Just a
maybeCombine f (Just a) (Just a') = Just $ f a a'

Это не очень важно для определять это локально, когда это необходимо, но, тем не менее, он является основным и общим, кажется, что должна быть стандартная реализация, но я могу Кажется, он не нашел.

Возможно, я просто что-то пропустил. То, что я хочу, кажется совершенно не связанным с поведением монады, поэтому я считаю, что ничего не найду в ящиках Monad/Arrow; но он точно напоминает экземпляр Monoid

Prelude Data.Monoid > Только "a" < > Nothing
Просто "а"  
Prelude Data.Monoid > Просто "a" < > Только "b"   
Просто "ab"   
...

... который, однако, требует, чтобы a был само моноидом, т.е. что он в основном имеет a->a->a "встроенный". Экземпляр MonadPlus также ведет себя так же, как я хочу, но он просто выбрасывает одно из значений, а не позволяет мне комбинировать функцию

Prelude Data.Monoid Control.Monad > Только 4 `mplus` Nothing
Всего 4 | Prelude Data.Monoid Control.Monad > Nothing `mplus` Just 4
Всего 4 | Prelude Data.Monoid Control.Monad > Просто 4 `mplus` Всего 5
Всего 4

Каким будет каноническое решение? Локальное сопоставление шаблонов? Что-то с комбинаторами из, например, Data.Maybe? Определение пользовательского моноида для объединения?

Ответ 1

Вы правы на деньги, заметив, что f похож на операцию Monoid в базовом типе a. Более конкретно, что происходит здесь, вы поднимаете Semigroup в Monoid путем смещения нуля (mempty), Nothing.

Это именно то, что вы видите в Haddocks для Maybe Monoid на самом деле.

Поднимите полугруппу в возможно формирование моноида в соответствии с http://en.wikipedia.org/wiki/Monoid: "Любая полугруппа S может быть превращена в моноид просто путем присоединения элемент e не в S и определяющий ee = e и es = s = s * e для всех s ∈ S." Поскольку нет класса "Semigroup", который предоставляет только mappend, мы вместо этого используем Monoid.

Или, если вам нравится пакет semigroups, тогда Option, который имеет именно такое поведение, соответствующим образом обобщенное для использования базового Semigroup.


Таким образом, самым ясным способом является определение экземпляра Monoid или Semigroup для базового типа a. Это чистый способ связать некоторый сумматор f с этим типом.

Что делать, если вы не контролируете этот тип, не хотите экземпляры-сироты и думаете, что оболочка newtype уродлива? Обычно вам не повезло, но это одно место, где используется общая черная магия, фактически GHC-only reflection пакет приходит удобно. Тщательные объяснения существуют в самой статье, но Ausin Seipp FP Complete Tutorial включает в себя некоторый пример кода, позволяющий вам "вводить" произвольные полугрупповые продукты в типы без (как много) шума определения типа... ценой гораздо более страшных сигнатур.  

Это, вероятно, значительно больше накладных расходов, чем его стоимость.

Ответ 2

Вы всегда можете использовать

f <$> m <*> n <|> m <|> n

Но это, к сожалению, не имеет канонической реализации где угодно.

Вы можете использовать reflection, чтобы получить (a -> a -> a) "испеченный в" как Semigroup для использования с Option, который предоставляется semigroups как улучшенная версия Maybe, которая имеет ' right "для Monoid в терминах Semigroup. Однако это слишком тяжело для этой проблемы. =)

Возможно, это должно быть просто добавлено как комбинатор к Data.Maybe.

Ответ 3

import Data.Monoid
maybeCombine :: (a->a->a) -> Maybe a -> Maybe a -> Maybe a
maybeCombine f mx my = let combine = mx >>= (\x -> my >>= (\y -> Just (f x y)))
                       in getFirst $ First combine `mappend` First mx `mappend` First` my

в GHCi, это дает мне

*Main> maybeCombine (+) Nothing Nothing
Nothing
*Main> maybeCombine (+) (Just 3) Nothing
Just 3
*Main> maybeCombine (+) (Just 3) (Just 5)
Just 8

Также работает с getLast, если вы поместите Last combine в конец последовательности mappend