Proposal: add liftA4 and liftA5 to match liftM4 and liftM5

David Feuer david.feuer at gmail.com
Sat Nov 8 20:21:25 UTC 2014


On Sat, Nov 8, 2014 at 2:39 PM, Edward Kmett <ekmett at gmail.com> wrote:

> We have two competing tensions that have been guiding the work so far,
> which is scattered across a few dozen different proposals and patches in
> Phab and is alarmingly intricate to detail.
>
> We've generally been guided by the notion you suggest here. In the wake of
> the AMP, the 'M' suffix really comes to mean the minimal set of effects
> needed to get the effect. This lets us generalize large numbers of
> combinators in Control.Monad (e.g. when/unless/guard) to 'just work' in
> more contexts.
>
> However, we also have to balance this carefully against two other concerns:
>
> 1.) Not breaking user code silently in undetectable ways.
>
> This is of course the utmost priority. It guides much of the AMP,
> including the 'backwards' direction of most of the dependencies. e.g. The
> reality is a large number of folks wrote (*>) = (>>) in their code, so e.g.
> if we defined (>>) = (*>), we'd get a large chunk of existing production
> code that just turns into infinite loops. We can always do more later as we
> find it is safe, but "first do no harm."
>
>
Indeed. I've looked at quite a few Applicative and Monad instances lately,
and one conclusion I've come to is that it often makes *more* sense to
define (*>) = (>>) than the other way around. In particular, monads like IO
and ST have a (>>=) that's about as simple as anything remotely interesting
you can do with them. In particular,

  fs <*> xs = fs >>= \f -> xs >>= \x -> return (f x)

is about as simple as it gets. The default definition of (*>) looks like
this:

  m *> n = (id <$ m) <*> n

But these don't have particularly special Functor instances either. So this
expands out to

  m *> n = fmap (const id) m <*> n

which becomes

  m *> n = (m >>= (return . const id)) >>= \f -> n >>= \x -> return (f x)

Can we say "ouch"? We now have to hope that GHC inlines enough to do
anything more. If it does, it will take a few extra steps along the way.

Compare this mess to (>>):

m >> n = m >>= \_ -> n

So I think there's a pretty clear case for (*>) = (>>) actually being the
right thing in a lot of cases.


> 2.) Not introducing rampant performance regressions.
>
> David Feuer has been spending untold hours on this, and his work there is
> a large part of the source of endless waves of proposals he's been putting
> forth.
>
> Considering `liftM2` as 'redundant' from `liftA2` can be dangerous on this
> front.
>

That's definitely a valid concern, for the reasons described above.
Everything works out nicely because of monad laws, but GHC doesn't know
that.



> The decision of if we can alias liftM3 to liftA3 needs to be at least
> *partially* guided by the question of whether the latter is a viable
> replacement in practice. I'm not prepared to come down on one side of that
> debate without more profiling data.
>

Yes, that makes sense. I think the problem fundamentally remains the
same--the monadic operations ultimately need to be inlined and completely
twisted around in order to be fast.


>
> Adding liftA4, liftA5 runs afoul of neither of these caveats. Aliasing
> liftMn to liftAn is something that *potentially* runs afoul of the
> latter, and is something that we don't have to do in a frantic rush. The
> world won't end if we play it safe and don't get to it in time for 7.10.
>

The more I think about it, the more right I think you are.

David
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://www.haskell.org/pipermail/libraries/attachments/20141108/56e1bc50/attachment.html>


More information about the Libraries mailing list