I find this deprecation to be very unfortunate:
/** Alias for [[productR]]. */
@deprecated("Use *> or productR instead.", "1.0.0-RC2")
@noop @inline final def followedBy[A, B](fa: F[A])(fb: F[B]): F[B] =
productR(fa)(fb)
/** Alias for [[productL]]. */
@deprecated("Use <* or productL instead.", "1.0.0-RC2")
@noop @inline final def forEffect[A, B](fa: F[A])(fb: F[B]): F[A] =
productL(fa)(fb)
While I agree with the reasoning given in https://github.com/typelevel/cats/issues/1983, as their definition should have been on FlatMap instead, since their naming implies some ordering of effects, the problem is that their deprecation has left a void waiting to be filled, being a case of throwing the baby with the bathwater.
*> is just plain ugly and personally I do not share the common wisdom of Haskell's community that naming is not important - it's a personal feeling for sure, so I'm sorry for my negativity, but I feel repulsed by such operators. At least the old >> and << were defensible due to having been used for streams in C++, but I haven't seen *> and <* and <*> until Cats and yes I stayed away from Scalaz precisely because of such line noise - a superficial reason to stay away from what are wonderful libraries for sure, but why shoot yourself in the foot and lose users over such trivialities?
I could use followedBy and forEffect without shame in ScalaDocs, tutorials or whenever I found myself in a situation where I had to give examples to people, they aren't necessarily obvious, but the link to flatMap and effects is intuitive.
Ever since they are deprecated I just can't see myself using *> or <*. These are not beginner friendly and do nothing but scare people, giving credence to the long held belief that Scala needs a periodic table of operators. They are not touch-typing friendly either; hitting * and < or > does not come as natural as typing followedBy and I believe this is a common problem due to touch typing tutorials not focusing enough on special characters, plus the position of those special chars shifts a lot depending on keyboard model, maker and targeted country.
Also productR and productL are terrible at expressing what was expressed by followedBy and forEffect.
In fact I'm actually scared of using productR and productL, simply because these operations are defined in terms of map2 and ap and not in terms of flatMap.
And even if there is a law that states that ap and map2 should be equivalent with flatMap when it comes to monadic types, I still fear using productR and productL simply because somebody out there might not think overriding them is important and that the equivalence in question should only hold for types that don't involve side effects. Just because of the name, just because they are not overridden in FlatMap or Monad.
IMO we should bring back followedBy and forEffect and maybe override them or move them to FlatMap to ensure ordering via flatMap.
PS: here's an instance where laws where broken due to this seemingly innocent switch — https://github.com/typelevel/cats-effect/issues/82
I’m -1 on moving things into FlatMap or Monad just because we fear someone wrote a broken typeclasses that violates the laws.
If we start with that where does it end?
their definition should have been on FlatMap instead, since their naming implies some ordering of effects
I'm confused. Applicative effects absolutely are ordered. The effects in a *> b are left to right just like a >> b.
@ List(1,2) <* List('a','b')
res1: List[Int] = List(1, 1, 2, 2)
@ List('a','b') *> List(1,2)
res2: List[Int] = List(1, 2, 1, 2)
I'm speaking of suspended side effects. Applicative cannot force ordering of side effects, either via its signature or laws. If it did, then cats.Parallel would not be possible or useful, depending on how you look at it.
We prefer to say that if the type has a Monad implementation (an extra restriction that comes after Applicative gets implemented), that we'd rather have coherence between ap / map2 and flatMap. That says nothing about the Applicative requirement however and we simply assume that side effects get ordered too, as if ap, map2 or product were described in terms of flatMap. But the laws do not speak of side effects. That's simply a generally agreed upon convention, which is wishful thinking.
Again, a concrete instance where this assumption broke things is here:
https://github.com/typelevel/cats-effect/issues/82
So to take IO as an example:
import cats._
import cats.effect._
import cats.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
import java.util.concurrent.atomic.AtomicInteger
def test: IO[Int] = IO.suspend {
val race = new AtomicInteger(0)
def parIO(n: Int) =
IO.Par(IO.shift.flatMap(_ => IO(race.compareAndSet(0, n))))
IO.Par.unwrap(
(parIO(1) *> parIO(2)).map { _ => race.get}
)
}
Traverse[List]
.sequence((0 until 10000).map(_ => test).toList)
.map { list =>
val nr = list.count(_ == 1)
(nr, list.length - nr)
}
.unsafeToFuture()
.foreach(println)
On my computer this sample has the wrong order in 16% of the cases, from a sample of 10000 runs.
Of course, this is IO.Par and for IO the *> operator has the correct behavior. But to me the behavior for IO.Par is enough to discount it as not being a shortcut of flatMap, when it is flatMap that you want. Because *> has nothing to do with flatMap and should not be used as such.
As for the argument that *> is very friendly to read, here's a conversation I had yesterday:
https://twitter.com/giuliohome_2017/status/968555094634586112
Of course, it's not a given that without *> that sample could have been understood, but in my mind there's a big difference between >>= and flatMap, between *> and followedBy. It's zero information versus something you can cling on to. Human brains are not very rational either, we get scared easily.
I know Haskell developers disagree, but Haskell doesn't have the greatest track record in getting adopted, so I'd take that philosophy with a grain of salt 🙂
I'm speaking of suspended side effects. Applicative cannot force ordering of side effects, either via its signature or laws. If it did, then cats.Parallel would not be possible or useful, depending on how you look at it.
:+1:
We can say that Monad[F].*> is the same as Monad[F].>>, but can't say anything about ordering of effects when we only have an Applicative constraint.
Otoh, followedBy is often just not the right name.
import fs2._
import cats.implicits._
import cats.effect.IO
Stream.repeatEval(IO(println("yo")).take(5) followedBy Stream.repeatEval(IO(println("lo")).take(5)
Doesn't do what you think it'll do.
big difference between >>= and flatMap, between *> and followedBy
productR : flatMap = followedBy : andThenComposeOrSomething
flatMap and productR describe what the operation is, but don't convey any false intuitions, unlike followedBy
How about bindL / bindR, or chainL / chainR? 🙂
Most helpful comment
I’m -1 on moving things into FlatMap or Monad just because we fear someone wrote a broken typeclasses that violates the laws.
If we start with that where does it end?