Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reverse the order of arguments to ap? #803

Closed
ceedubs opened this issue Jan 14, 2016 · 10 comments
Closed

Reverse the order of arguments to ap? #803

ceedubs opened this issue Jan 14, 2016 · 10 comments

Comments

@ceedubs
Copy link
Contributor

ceedubs commented Jan 14, 2016

Currently the function signature for Apply.ap looks like this:

  def ap[A, B](fa: F[A])(ff: F[A => B]): F[B]

However, there's a requirement (an unwritten one as far as I know) that the F effect for the second argument (ff) is applied before the effect of the first argument (fa). This dependency is relied upon for things like an implementation of foldMap in terms of Const.

This reverse order of effects has tripped us up a few times.

While we generally prefer to put functions as the last argument to a method, I recommend that we make an exception here and put ff before fa in the ap definition to help avoid this confusion. Also, since the ff function is wrapped in an F context, the usual arguments for putting the function last probably don't apply quite as strongly. I believe @mandubian also is in favor of this change (correct me if I'm wrong, Pascal).

The proposed new signature would be:

  def ap[A, B](ff: F[A => B], fa: F[A]): F[B]

Or we could continue to use two parameter lists, but I don't know if there's any advantage in this case.

@mandubian
Copy link
Contributor

I'm in favor of it as correction has already been made to be consistent with Haskell version but until we reverse params order, we can't be consistent and it leads to not very logic behavior as you've experimented yourself...

@julien-truffaut
Copy link
Contributor

I agree, it seems that reordering ap arguments will make effect order more clear.

@ceedubs
Copy link
Contributor Author

ceedubs commented Jan 14, 2016

Okay we have at least 3 votes for this. Let's consider it ready for a PR.

@lukewyman
Copy link
Contributor

And it looks like truly low-hanging fruit, so I'll take it :)

@lukewyman
Copy link
Contributor

Looking for feedback

I set about making the changes as specified in this ticket with the uncurried signature of:
def ap[A, B](ff: F[A => B], fa: F[A]): F[B].

I ran into the type inference problems that can happen with this kind of signature, so I switched to the curried version to remedy that, as follows:
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

I was able to make all the necessary changes project-wide with this solution, except for the generated code in the ApplyArityFunctions, which always resulted in type inference compile errors. I coded up a little experiment to get to the bottom of this, and also to isolate the problem from the task of codegen, and found the following:

With our original signature of def ap[A, B](fa: F[A])(ff: F[A => B]): F[B], type inference in the implementation works fine (look at a0, a1, a2):

def ap3[A, B, C, Z](fa: F[A], fb: F[B], fc: F[C])(ff: F[(A, B, C) => Z]): F[Z] =
ap2(fb,fc)(ap(fa)(map(ff)(f =>
(a0) => (a1, a2) => f(a0, a1, a2)
)))

But, with the new signature of def ap[A, B](ff: F[A => B])(fa: F[A]): F[B], type inference doesn't work, and the implementation needs some help, like this:

def ap3[A, B, C, Z](ff: F[%28A, B, C%29 => Z])(fa: F[A], fb: F[B], fc: F[C]): F[Z] =
ap2R(apR(map(ff)(f =>
(a0: A) => (a1: B, a2: C) => f(a0,a1,a2)
))(fa))(fb,fc)

Given all this, should I:

  1. go ahead and make the necessary changes to Boilerplate so that the ApplyArityFunctions have their extra type inference help and our Apply.ap signature is as we want it, OR
  2. is there something wrong with the implementation of the arity functions needing this extra type inference help and thus we should reconsider?

If we go with 1), I should be able to wrap this up in fairly short order.

@aryairani
Copy link
Contributor

@lukewyman Gotta edit / put those bad boys into code blocks:

//...

i.e.

def ap[A,B,C,Z](ff: F[(A,  B, C)] => Z]): F[Z]

vs
def ap[A,B,C,Z](ff: F[%28A, B, C%29] => Z]): F[Z]

can use ... for inline code a0, a1, ...

@lukewyman
Copy link
Contributor

That was my mistake on Waffle. I didn't know how to format stuff on here, so the code blocks went away. My syntax is correct in real life. The original problem is legit.

@lukewyman
Copy link
Contributor

Ok, so I've finished making all the changes to core for this item. The issue is with the laws. I'm wondering how much of this is just fixing a little something, or if this may have to do with why the parameter lists for Apply.ap are in the order they are for Scala (vs. Haskell order).

The law (as is) is stated as follows, with ap being a member of fa. (fa used to be first in the parameter order for Apply.ap, and now with the change, it comes second):

def applyComposition[A, B, C](fa: F[A], fab: F[A => B], fbc: F[B => C]): IsEq[F[C]] = {
    val compose: (B => C) => (A => B) => (A => C) = _.compose
    fa.ap(fab).ap(fbc) <-> fa.ap(fab.ap(fbc.map(compose)))
  }

And the error for the law with the change is:

cats/laws/ApplyLaws.scala:15: Cannot prove that A => B =:= ((A => B) => (A => C)) => B.
[error]     fa.ap(fab).ap(fbc) <-> fa.ap(fab.ap(fbc.map(compose)))

Clearly, this would be false anyway, but is there a way to change how we state the law accordingly, or is the problem deeper?

lukewyman added a commit to lukewyman/cats that referenced this issue Jan 20, 2016
@liff
Copy link
Contributor

liff commented Jan 20, 2016

I was kind of curious and looked into this. My guess is it might have something to do with simulacrum-generated syntax since that's the only thing that I can find is using =:=.

The @typeclass generated companion for Apply looks like this (via -Ymacro-debug-lite):

  //
  object Apply extends scala.AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    @new inline() def apply[F[_]](implicit instance: Apply[F]): Apply[F] = instance;
    abstract trait Ops[F[_], C] extends scala.AnyRef {
      def $init$() = {
        ()
      };
      val typeClassInstance: Apply[F];
      def self: F[C];
      // the =:= is being used here ($eq$colon$eq)
      def ap[A, B](fa: F[A])(implicit ev$macro$2: $eq$colon$eq[C, _root_.scala.Function1[A, B]]) = typeClassInstance.ap(self.asInstanceOf[F[_root_.scala.Function1[A, B]]])(fa);
      def ap2[A, B, Z](fa: F[A], fb: F[B])(implicit ev$macro$3: $eq$colon$eq[C, _root_.scala.Function2[A, B, Z]]) = typeClassInstance.ap2(self.asInstanceOf[F[_root_.scala.Function2[A, B, Z]]])(fa, fb);
      def map2[B, Z](fb: F[B])(f: _root_.scala.Function2[C, B, Z]) = typeClassInstance.map2(self, fb)(f)
    };
  //

I think the origin of =:= evidence usage is from this line in simulacrum.

It seems you might be able to avoid the problem by not using syntax but it's probably not the right way to go in the long run.

@ceedubs
Copy link
Contributor Author

ceedubs commented Feb 3, 2016

This was resolved by #833

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants