Andrew Martin
2017-11-28 18:14:00 UTC
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)
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/
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.
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.
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 thewhole 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 asdescribed 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
-Andrew Thaddeus Martin