Thanks for your replies.
My hope for AMP was to get generalization of effectful combinators without requiring more identifiers (and actually freeing some, like making `sequenceA` and `for` obsolete). I see that it is not so easy if GHC's reduction behavior has to be taken into account.
So +1 from me if you deem liftA4 and liftA5 necessary.
On 08.11.2014 21:21, David Feuer wrote:
On Sat, Nov 8, 2014 at 2:39 PM, Edward Kmett <ekmett@gmail.comleast /partially/ guided by the question of whether the latter is a<mailto:ekmett@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
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
--
Andreas Abel <>< Du bist der geliebte Mensch.
Department of Computer Science and Engineering
Chalmers and Gothenburg University, Sweden
andreas.abel@gu.se
http://www2.tcs.ifi.lmu.de/~abel/