Monoid Instance for ReaderT

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|

Monoid Instance for ReaderT

Andrew Martin
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) concludes with:

> 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

_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Reply | Threaded
Open this post in threaded view
|

Re: Monoid Instance for ReaderT

M Farkas-Dyck-2
On 28/11/2017, Andrew Martin <[hidden email]> wrote:
>     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.
_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Reply | Threaded
Open this post in threaded view
|

Re: Monoid Instance for ReaderT

Elliot Cameron-2
+1

On Tue, Nov 28, 2017 at 2:22 PM, M Farkas-Dyck <[hidden email]> wrote:
On 28/11/2017, Andrew Martin <[hidden email]> wrote:
>     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.
_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries


_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Reply | Threaded
Open this post in threaded view
|

Re: Monoid Instance for ReaderT

Andrew Martin
In reply to this post by M Farkas-Dyck-2
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.

On Tue, Nov 28, 2017 at 2:22 PM, M Farkas-Dyck <[hidden email]> wrote:
On 28/11/2017, Andrew Martin <[hidden email]> wrote:
>     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.



--
-Andrew Thaddeus Martin

_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Reply | Threaded
Open this post in threaded view
|

Re: Monoid Instance for ReaderT

M Farkas-Dyck-2
On 28/11/2017, Andrew Martin <[hidden email]> wrote:
> 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.)
_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries
Reply | Threaded
Open this post in threaded view
|

Re: Monoid Instance for ReaderT

Andrew Martin
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

> On Nov 28, 2017, at 5:18 PM, M Farkas-Dyck <[hidden email]> wrote:
>
>> On 28/11/2017, Andrew Martin <[hidden email]> wrote:
>> 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.)
_______________________________________________
Libraries mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/libraries