user6445533
user6445533

Reputation:

Why is Maybe's Semigroup instance biased towards Just and the Monoid uses Nothing as its empty element?

Maybe expresses computations that might not produce a result due to an error. Therefore such computations must be short circuited.

Now Maybe's Semigroup/Monoid instances seem to break with this semantics, because the former is biased towards Just and the latter treats the error case Nothing as its empty element:

Just "foo" <> Nothing -- Just "foo"
Nothing <> Just "bar" -- Just "bar"
Just "foo" <> Just "bar" -- Just "foobar"
Nothing <> Nothing -- Nothing

I would expect Nothing for the first two cases.

Here is the alternative implementation (hopefully it is correct/lawful):

instance Semigroup a => Semigroup (Maybe a) where
    Nothing <> _       = Nothing
    _       <> Nothing = Nothing
    Just a  <> Just b  = Just (a <> b)

instance Monoid a => Monoid (Maybe a) where
  mempty = Just mempty

I don't want to say that these alternative instances are better. But they seem useful too. So why was a selection made in the first place instead of leaving the implementation to the user?

Upvotes: 7

Views: 1245

Answers (2)

leftaroundabout
leftaroundabout

Reputation: 120711

I think the idea behind this is that Maybe is supposed to have the semantics that are actually only properly expressed by Option: it takes any semigroup and makes a monoid out of it, by adding Nothing as an ad-hoc “free mempty”. I.e. actually the instances should be

instance Semigroup a => Semigroup (Maybe a) where
  Nothing <> a = a
  a <> Nothing = a
  Just x <> Just y = Just $ x <> y

instance Semigroup a => Monoid (Maybe a) where
  mappend = (<>)
  mempty = Nothing

This is analogous to how [] gives the free monoid over any type, by adding both mempty and <>.

Of course, this requires that the Semigroup class is in base, which it wasn't until recently.

Corollary to this is that mempty becomes clearly pattern-matchable, because by parametricity it cannot depend on the contained type.

Practically speaking, this instance is arguably more useful then the one you propose, because as Luke remarked that is already covered by the Applicative instance so you can easily write liftA2 (<>) and pure mempty. Granted, the standard instance is also already covered by the Alternative instance, but Alternative/MonadPlus have always been considered as a bit hackish, inferior to the more mathematically impeccable Semigroup, Monoid, Functor and Applicative.

Upvotes: 3

luqui
luqui

Reputation: 60463

Your instance is actually a special case of a much more general instance for applicative functors.

newtype LiftA f a = LiftA { getLiftA :: f a }

instance (Applicative f, Semigroup a) => Semigroup (LiftA f a) where
    LiftA x <> LiftA y = LiftA $ liftA2 (<>) x y

instance (Applicative f, Monoid a) => Monoid (LiftA f a) where
    mempty = LiftA $ pure mempty

Which I assumed would be in the standard library somewhere (under a different name probably) but I couldn't find it. But the existence of this general instance could be one reason to choose the library version of Maybe, which is more of Maybe's special power. On the other hand, it's quite nice when your algebraic structures are all coherent with each other; i.e. when a type is an Applicative, use the "LiftA"-style instance whenever possible (on all F-algebra classes).

On the third hand (!), we can't have coherence everywhere, since the library instance agrees with Maybe's MonadPlus instance. This is strikingly parallel to the fact that there are two monoids on natural numbers: addition and multiplication. For numbers, we just picked not to have any monoid instance because it's unclear which to use.

In conclusion, I don't know. But maybe this information was helpful.

Upvotes: 8

Related Questions