Discussion:
Monoid Instance for ReaderT
Andrew Martin
2017-11-28 18:14:00 UTC
Permalink
There's a thread from earlier this year where someone asked about a Monoid
instance for ReaderT:
https://mail.haskell.org/pipermail/haskell-cafe/2017-March/126588.html

Several other people showed interest in the instance, but in the end, the
request was dismissed since there are other valid monoid instances for
ReaderT. Conseqently, the PR was closed (
https://hub.darcs.net/ross/transformers/issue/38). This final comment (
https://mail.haskell.org/pipermail/haskell-cafe/2017-March/126605.html)
In the absence of a principled reason to prefer one over the others and a
general consensus, I think it’s better not to choose.

This is the point I'd like to argue. There were three possible monoid
instances for ReaderT discussed. They are:

instance (Applicative m, Monoid a) => Monoid (ReaderT r m a)
instance (Alternative m) => Monoid (ReaderT r m a)
instance (Monoid (m a)) => Monoid (ReaderT r m a)

I believe there is good reason to prefer the first instance over the other
two. The third instance requires FlexibleContexts. This takes it outside
the realm of Haskell98 and Haskell2010, which the transformers library
stays within. In fact, transformers pioneered the Eq1, Ord1, etc. classes
that ended up in base specifically to avoid FlexibleContexts, and deviating
from this now seems against the spirit of the library, since transformers
intends that it is able to be built with any Haskell compiler, not just GHC.

The Alternative-based instance is reasonable, but we already have another
instance that for ReaderT that is implemented this way: the Alternative
instance! Here it is:

instance Alternative m => Alternative (ReaderT r m)

Here I'm going to loosely adapt an argument that Gabriel Gonzalez makes
when defending his Monoid instance for ListT (
https://www.reddit.com/r/haskell/comments/4r5bcj/listtransformer_a_beginnerfriendly_listt/d4z0i55/
Also, with the Alternative class the only thing you can do is concatenate
collections (there's no way to write the cartesian product because the
whole class is higher-kinded) so it's meaning is always unambiguous.
However, with the Monoid class there are two possible behaviors (append or
cartesian product). Given that the Alternative class has to be append, I
prefer to give the Monoid class the other behavior. From the end user's
perspective, if you use Alternative you always know exactly what you are
getting so there is no disadvantage to using Alternative.
The other reason that it's nice to give the Monoid class the other
behavior is that it works really nicely when chaining monoid instances as
described in this post.

The Alternative instance for ReaderT has to be Alternative-based. There is
no way around this. Given that we have two possibilities for the Monoid
instance, I would prefer to see the one that gives us something different
from an instance we already have.

Why is it beneficial to have the different behavior instead of the existing
behavior? In part, it's because we now have a greater number of behaviors
from the typeclasses that most people are familiar with. Additionally, we
actually end up in a better situation when the Monoid instance isn't what
we need. Let's consider two scenarios to measure the inconvenience that a
user faces when the Monoid instance isn't the one that they want:

1) The Monoid instance for ReaderT, WriterT, StateT, etc. is Monoid-based
but we wish are in a scenario where we want the Alternative-based one.
2) The Monoid instance for ReaderT, WriterT, StateT, etc. is
Alternative-based but we wish are in a scenario where we want the
Monoid-based one.

In scenario (1), the workaround is trivial. In Data.Monoid, we have Alt,
which recovers a Monoid instance from an Alternative instance:

newtype Alt f a = Alt {getAlt :: f a}
instance Alternative f => Monoid (Alt f a) where
mempty = Alt empty
mappend = coerce ((<|>) :: f a -> f a -> f a)

The same Alt data type is reusable with ReaderT, WriterT, and StateT to
recover the Alternative-based instance we wanted:

myActions :: [ReaderT IO ()]
runUntilSuccess :: ReaderT IO ()
runUntilSuccess = coerce (fold (coerce myActions :: [Alt (ReaderT IO)
()]))

But what about scenario (2)? Let's say that we now instead want the
Monoid-based instance that requires all of the actions to succeed.

myActions :: [ReaderT IO ()]
runAll :: ReaderT IO ()
runAll = ...

What do we put in place of the ellipsis? We can just write it out by hand,
but it would be nicer if this Monoid instance was reusable in some way.
Let's say that we introduce a newtype wrapper for the Monoid-based Monoid
instance:

newtype MonoidReaderT m a = MonoidReaderT ...
instance (Applicative m, Monoid a) => Monoid (MonoidReaderT m a)

But now if we want something like this for StateT or WriterT, they're going
to need their own newtype wrappers as well. That's unfortunate. In scenario
(1), we needed a single newtype wrapper (that already exists in base!) to
recover everything that we lost. In scenario (2), we need a newtype wrapper
per type. So, by providing an instance that gives us more behaviors from
haskell's blessed set of typeclasses, we end up in a better situation when
we need to recover what we don't have.

Finally, and this is the weakest part of my argument since it's pure
speculation, I think that the Monoid-based instance is more useful in
practice. For some loosely analogous historical precedent, I would point to
the Monoid instance for IO. In July 2014, Edward Kmett pointed out that one
issue with admitting a Monoid instance for IO was that two possible
instances existed (
https://mail.haskell.org/pipermail/glasgow-haskell-users/2014-July/025124.html).
One of them monoidally concatenated the results, and the one was identical
to IO's Alternative instance. Sounds familiar. Ultimately though, in
November 2014, when Gabriel Gonzalez proposed the instance that monoidally
concatenated results, nobody objected (
https://mail.haskell.org/pipermail/libraries/2014-November/024310.html).
Every person who responded on the mailing list wanted the Monoid-based
instance, not the one that was a copy of Alternative. (And it's always
recoverable via Alt in the event that someone does want it). So, and again
I want to stress that I consider this a weaker part of the argument since
it's more rooted in human preferences rather than logic, I think the
Monoid-based instance is just generally more useful in practice. In my own
experience, I often have wanted the Monoid-based instances, but I have
never wanted the Alternative-based instances

With all that said, I would like to ask that the Monoid-based Monoid
instances for ReaderT, WriterT, StateT, AccumT, etc. be considered. I would
also appreciate any feedback or further discussion of this issue.
--
-Andrew Thaddeus Martin
M Farkas-Dyck
2017-11-28 19:22:48 UTC
Permalink
Post by Andrew Martin
instance (Applicative m, Monoid a) => Monoid (ReaderT r m a)
I believe there is good reason to prefer the first instance over the other
two.
+1

This is also how the `Monoid` instance of `(->)` is defined.
Elliot Cameron
2017-11-28 19:34:57 UTC
Permalink
+1
Post by M Farkas-Dyck
Post by Andrew Martin
instance (Applicative m, Monoid a) => Monoid (ReaderT r m a)
I believe there is good reason to prefer the first instance over the
other
Post by Andrew Martin
two.
+1
This is also how the `Monoid` instance of `(->)` is defined.
_______________________________________________
Libraries mailing list
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Andrew Martin
2017-11-28 21:38:10 UTC
Permalink
This is also how the Monoid instance of basically everything with an
Applicative instance is defined. The only one that I feel should differ
from the others is Maybe, and that's getting fixed when Semigroup becomes a
superclass of Monoid.
Post by M Farkas-Dyck
Post by Andrew Martin
instance (Applicative m, Monoid a) => Monoid (ReaderT r m a)
I believe there is good reason to prefer the first instance over the
other
Post by Andrew Martin
two.
+1
This is also how the `Monoid` instance of `(->)` is defined.
--
-Andrew Thaddeus Martin
M Farkas-Dyck
2017-11-28 22:18:43 UTC
Permalink
Post by Andrew Martin
This is also how the Monoid instance of basically everything with an
Applicative instance is defined. The only one that I feel should differ
from the others is Maybe, and that's getting fixed when Semigroup becomes a
superclass of Monoid.
I also feel the `[]` instance ought to do the Cartesian product. (To
recover the free monoid one would write `Alt []` rather than `[]`
which is fine by me.)
Andrew Martin
2017-11-28 23:28:47 UTC
Permalink
Ah, I had forgotten about that one. Yeah, I wish it did Cartesian product too. Although somehow, I never actually find myself using the Monoid instance for list at all. But, changing the Monoid instance for list is not a battle I want to fight, now or ever. The breakage, both of code and of tutorials that talk about the free Monoid, would just be enormous. I’ve decided instead to focus on improving the Monoid instance for Map, which I think could actually be done without as much trouble. But anyway, that’s a topic for another thread.

Sent from my iPhone
Post by M Farkas-Dyck
Post by Andrew Martin
This is also how the Monoid instance of basically everything with an
Applicative instance is defined. The only one that I feel should differ
from the others is Maybe, and that's getting fixed when Semigroup becomes a
superclass of Monoid.
I also feel the `[]` instance ought to do the Cartesian product. (To
recover the free monoid one would write `Alt []` rather than `[]`
which is fine by me.)
Loading...