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

FreeApplicative applies effects in wrong order #799

Closed
ceedubs opened this issue Jan 12, 2016 · 15 comments
Closed

FreeApplicative applies effects in wrong order #799

ceedubs opened this issue Jan 12, 2016 · 15 comments
Assignees

Comments

@ceedubs
Copy link
Contributor

ceedubs commented Jan 12, 2016

Here is a fairly minimal way to reproduce the error:

import cats._
import cats.free.FreeApplicative
import cats.arrow.NaturalTransformation
import cats.implicits._

type Dsl[A] = FreeApplicative[Id, A]
val x: Dsl[String] = FreeApplicative.lift[Id, String]("x")
val y: Dsl[String] = FreeApplicative.lift[Id, String]("y")

// either one produces the wrong output, so it's not a problem with the |@| syntax
//val z: Dsl[Unit] = (x |@| y).map((_, _) => ())
val z = Apply[Dsl].map2(x, y)((_, _) => ())

val asString: Id ~> λ[α => String] = new (Id ~> λ[α => String]) {
  def apply[A](a: A): String = a.toString
}

// produces "yx" instead of "xy"
z.analyze(asString)

I believe this is a bug in the ap implementation.

Scalaz's implementation of FreeAp does the same thing, so it's possible that I'm wrong about what the expected result should be, but I don't think so. cc @runarorama

@adelbertc, @mandubian, or @raulraja would any of you want to take a look at this by any chance?

@ceedubs ceedubs added the ready label Jan 12, 2016
@ceedubs
Copy link
Contributor Author

ceedubs commented Jan 12, 2016

Hmm this might actually be because of how foldMap is implemented (which analyze uses).

@mandubian
Copy link
Contributor

Wouldn't it be due to the way Const Applicative ap is implemented?

https://github.com/non/cats/blob/master/core/src/main/scala/cats/data/Const.scala#L106-L108

@ceedubs
Copy link
Contributor Author

ceedubs commented Jan 12, 2016

@mandubian I am pretty sure Const's ap now uses the correct order. It used to be wrong but that was fixed in non@0fb2826. ap is supposed to apply effects in the opposite order of the parameters, which is a bit confusing.

@mandubian
Copy link
Contributor

not yet in master, right?

@ceedubs
Copy link
Contributor Author

ceedubs commented Jan 12, 2016

@mandubian It should be. It was merged with #502 I believe.

@mandubian
Copy link
Contributor

oh no, you mean it's normal to be in the opposite and it's right in master :)
The z structure looks like:

z: Dsl[Unit] = Ap(x,Ap(y,Pure(<function1>)))

I've added some dummy println to check order:

scala> z.analyze(asString)
foldMap -> pivot:x
foldMap -> pivot:y
Const.app -> fa:Const(y) f:Const() r:Const(y)
Const.app -> fa:Const(x) f:Const(y) r:Const(yx)
res0: String = yx

So it seems to execute as expected...

@mandubian
Copy link
Contributor

I don't really understand why we have made this modification for apas in Haskell, the signature is:

(<*>) :: f (a -> b) -> f a -> f b

and in cats it's

def ap[A, B](fa: Const[C, A])(f: Const[C, A => B]): Const[C, B]

so f & fa should have been reversed not the order of execution of f & fa...

and in haskell it is clearly considered that order of execution in an applicative has been chosen from left to right (even if in theory, there is no order, it should just be deterministic)

@ceedubs
Copy link
Contributor Author

ceedubs commented Jan 12, 2016

@mandubian I think the important thing is that the effect of the F[A => B] occurs before the effect of F[A], which I believe happens in the current implementation, doesn't it?

@mandubian
Copy link
Contributor

I've looked at Haskell Const applicative and yes it seems to apply function before...
but it means our Applicative implementation executes the function before the pivot and this is counter-intuitive compared to what you write and it is normal that you obtain yx

@adelbertc
Copy link
Contributor

I ran into this issue today with: https://github.com/adelbertc/cats/blob/monoidal-redux/laws/src/main/scala/cats/laws/ApplicativeLaws.scala#L36

My initial impression was to do:

F.ap(fa)(f) <-> F.map(F.product(fa, f)) { case (a, f) => f(a) }

but had to change to this to account for the evaluation of effects of F[A => B] vs. F[A]

F.ap(fa)(f) <-> F.map(F.product(f, fa)) { case (f, a) => f(a) }

@LiamGoodacre
Copy link

Just as a side note, the free applicative implementation of purescript had this issue too: ethul/purescript-freeap#4

@mandubian
Copy link
Contributor

@adelbertc it seems to be implemented as is if I'm not wrong... you write ap(fa)(f) and then f is executed before fa which seems not logical, right? (in haskell it would be ap(f)(fa) and f would be executed before fa which is expected)

@ceedubs
Copy link
Contributor Author

ceedubs commented Jan 13, 2016

@LiamGoodacre thanks for the reference, that's helpful!

@mandubian yes, in ap, f's effects should happen before fa's. However, I think the issue is that foldMap on FreeApplicative was passing the "tail" in as the second argument to ap, so the "tail" effects were coming before the "head".

I've changed the following line of the foldMap implementation:

case Ap(pivot, fn) => G.ap(f(pivot))(fn.foldMap(f))

to this:

case Ap(pivot, fn) => G.map2(f(pivot), fn.foldMap(f))((a, g) => g(a))

This seems to fix the issue. There may be a more elegant or efficient way of writing this. Should I go ahead and submit a PR for this?

@mandubian
Copy link
Contributor

my problem for now is that it's completely error prone... nobody will remind that when calling ap(a)(f), f is executed before a... and map2 does it in the left to right way...

@ceedubs
Copy link
Contributor Author

ceedubs commented Jan 13, 2016

@mandubian that's a fair concern. Should we change the signature of ap to take f as the first argument and fa as the second?

I do think that's a separate issue from this one, though. I'm inclined to address each in different PRs.

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

No branches or pull requests

5 participants