11

What I'm trying to do is trivial to define by hand, basically

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'

It's not a big deal to define this locally when needed, but still cumbersone and being so basic and general it seems there should be a standard implementation, yet I can't seem to find one.

Perhaps I'm just overlooking something. What I want seems quite unrelated on the behaviour of the maybe monad, so I reckon I won't find anything in the Monad/Arrow drawers; but it sure resembles the Monoid instance

Prelude Data.Monoid> Just "a" <> Nothing
Just "a"
Prelude Data.Monoid> Just "a" <> Just "b"
Just "ab"
...

...which however requires a to be a monoid itself, i.e. that it basically has the a->a->a "built in". The MonadPlus instance also behaves much like I want, but it simply throws away one of the values rather than allowing me to supply a combination function

Prelude Data.Monoid Control.Monad> Just 4 `mplus` Nothing
Just 4
Prelude Data.Monoid Control.Monad> Nothing `mplus` Just 4
Just 4
Prelude Data.Monoid Control.Monad> Just 4 `mplus` Just 5
Just 4

What would be the canonical solution? Local pattern matching? Something with combinators from e.g. Data.Maybe? Defining a custom monoid to do the combining?

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319

3 Answers3

12

You can always use

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

But this, sadly, doesn't have a canonical implementation anywhere.

You can use reflection to get that (a -> a -> a) "baked in" as the Semigroup for use with Option, which is provided by semigroups as an improved version of Maybe that has the 'right' instance for Monoid in terms of Semigroup. This is rather too heavy handed for this problem though. =)

Perhaps this should just be added as a combinator to Data.Maybe.

Edward Kmett
  • 29,632
  • 7
  • 85
  • 107
11

You're right on the money when you notice that the f is like a Monoid operation on the underlying a type. More specifically what's going on here is you're lifting a Semigroup into a Monoid by adjoining a zero (mempty), Nothing.

This is exactly what you see in the Haddocks for the Maybe Monoid actually.

Lift a semigroup into Maybe forming a Monoid according to http://en.wikipedia.org/wiki/Monoid: "Any semigroup S may be turned into a monoid simply by adjoining an element e not in S and defining ee = e and es = s = s*e for all s ∈ S." Since there is no "Semigroup" typeclass providing just mappend, we use Monoid instead.

Or, if you like the semigroups package, then there's Option which has exactly this behavior, suitably generalized to use an underlying Semigroup instead.


So that suggests the clearest way is to define either a Monoid or Semigroup instance on the underlying type a. It's a clean way to associate some combiner f with that type.

What if you don't control that type, don't want orphan instances, and think a newtype wrapper is ugly? Normally you'd be out of luck, but this is one place where using the total black magic, effectively GHC-only reflection package comes in handy. Thorough explanations exist in the paper itself but Ausin Seipp's FP Complete Tutorial includes some example code to allow you to "inject" arbitrary semigroup products into types without (as much) type definition noise... at the cost of a lot scarier signatures. 

That's probably significantly more overhead than its worth, however.

Teodor
  • 749
  • 7
  • 15
J. Abrahamson
  • 72,246
  • 9
  • 135
  • 180
  • 1
    I'm not out of luck, in the problem I'm working on right now I can in fact use the `Max` semigroup and get a really nice solution! – leftaroundabout Oct 31 '13 at 12:53
  • 3
    Awesome! I often end up defining `Option (Max a)` as adjoining a "negative infinity" on to a type, so absolutely. I think that's a really elegant `Monoid`. – J. Abrahamson Oct 31 '13 at 12:58
2
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

in GHCi, this gives me

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

Also works with getLast if you put Last combine at the end of the mappend sequence

itsbruce
  • 4,825
  • 26
  • 35
  • I concede this isn't as generic a solution as J. Abrahamson's ;) – itsbruce Oct 31 '13 at 13:39
  • +1 for answering a previously unadressed (and perhaps the most straightforward) aspect of this question... but honestly, this implementation is neither shorter nor more readable than my original stupid pattern-matching one. I also can't quite see how it might be better adaptable to variations in the requirement. – leftaroundabout Oct 31 '13 at 13:40
  • 1
    You can golf this method down to something almost as pithy as Ed's solution—`getFirst . mconcat . map First $ [liftA2 f mx my, mx, my]`, or even replace `getFirst . mconcat . map First` with `msum` though it's arguably less clear without the `First`. – J. Abrahamson Oct 31 '13 at 13:54
  • 1
    @leftaroundabout It would be prettier with `do` notation and I know it could be made shorter with lifting (thanks, J ;) but I just wanted to make the basic technique clear. – itsbruce Oct 31 '13 at 14:00
  • @leftaroundabout I also don't see any justice in " I also can't quite see how it might be better adaptable to variations in the requirement." Your current requirements can be expressed purely in terms of the Maybe monad; if you change the requirements but they are still expressible via Maybe, this technique can be extended to fit. If not, not. You want a solution that would generically fit *any* arbitrary variation in requirements? Well, OK, it's called "Haskell" – itsbruce Oct 31 '13 at 14:10
  • No no... I just meant, I don't see any application where your suggestion would actually be better than either of the previous ones. It's still a useful answer and all, and I think the `First` monoid is an interesting and unexpected consideration that might come in handy in other problems. – Only, I then couldn't think of any such problem that would look anything like the one in this question, hence my probably confusing remark. – leftaroundabout Oct 31 '13 at 14:18
  • Note about [First](https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Monoid.html#t:First): "This type will be marked **deprecated** in GHC 8.8, and removed in GHC 8.10. Users are advised to use the variant from Data.Semigroup and wrap it in Maybe." – user905686 Nov 18 '19 at 11:37