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

Applicative whenA/unlessA syntax extensions should respect call-by-name parameter #3899

Merged
merged 1 commit into from
May 23, 2022

Conversation

catostrophe
Copy link
Contributor

The current implementation of the .whenA/.unlessA syntax extensions doesn't respect F[A] being passed as call-by-name.

This leads to issues where it's important.
See: typelevel/fs2#2412

Unfortunately we can't change whenA/unlessA directly due to bincompat guarantees.
But the new method names even better reflect that we throw A out (as in traverse_). I would also deprecate the old ones.

Closes #3687

@@ -17,3 +19,8 @@ final class ApplicativeOps[F[_], A](private val fa: F[A]) extends AnyVal {
def unlessA(cond: Boolean)(implicit F: Applicative[F]): F[Unit] = F.unlessA(cond)(fa)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also suggest deprecating these two

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just make the old syntax not implicit and substitute the new one with the same names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joroKr21 then replicateA will become a syntax extenstion on => F[A], while the method itself accepts F[A]. Is it ok?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm not sure

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@catostrophe how about replicateA(0)? Shouldn't that also not execute fa?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but replicateA in Applicative takes a call-by-value param

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and exposing a by-name API for replicateA might mislead users to think of a more imperative style of API like List.fill where => F[A] might have side-effects.

Another option would be to extract ApplicativeStrictOps for replicateA

Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some test coverage?

Also I think this should probably be added to the Applicative trait and then the syntax is calling it so it can potentially be specialized by different type constructors when some efficiency can be gained.

@@ -17,3 +19,8 @@ final class ApplicativeOps[F[_], A](private val fa: F[A]) extends AnyVal {
def unlessA(cond: Boolean)(implicit F: Applicative[F]): F[Unit] = F.unlessA(cond)(fa)
def whenA(cond: Boolean)(implicit F: Applicative[F]): F[Unit] = F.whenA(cond)(fa)
}

final class ApplicativeByNameOps[F[_], A](private val fa: () => F[A]) extends AnyVal {
def unlessA_(cond: Boolean)(implicit F: Applicative[F]): F[Unit] = F.unlessA(cond)(fa())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use .void on these two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, what for?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, I got confused. Often we have foo_ as a variant of foo that discards the value. But I see that here we are doing something else (just using it as another version that uses call by name correctly.

I would like to see another name since, I think, that pattern is established and this would be a violation of it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐑 Um, if you think of a naming variant convention for "like X but call-by-name" we'd like to use it in mouse too, similar situation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I offer suffix L for Lazy, thus whenAL & unlessAL. It's short, and the reason for the L is at least sane, although not immediately self-evident

@@ -17,3 +19,8 @@ final class ApplicativeOps[F[_], A](private val fa: F[A]) extends AnyVal {
def unlessA(cond: Boolean)(implicit F: Applicative[F]): F[Unit] = F.unlessA(cond)(fa)
def whenA(cond: Boolean)(implicit F: Applicative[F]): F[Unit] = F.whenA(cond)(fa)
}

final class ApplicativeByNameOps[F[_], A](private val fa: () => F[A]) extends AnyVal {
Copy link
Contributor

@satorg satorg Nov 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a minor thing, but wouldn't it be better to keep constructors of *-Ops classes private to the syntax package? I know, not all the existing classes do follow it, but perhaps it is a good idea for all new classes, wdyt?

final class ApplicativeByNameOps[F[_], A] private[syntax] (private val fa: () => F[A])
  extends AnyVal

I mean, it is unlikely that we want to allow users to create Ops files explicitly, isn't? Or is there some use case for that?

@armanbilge armanbilge merged commit f47d50d into typelevel:main May 23, 2022
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

Successfully merging this pull request may close these issues.

whenA and unlessA by-name makes syntax inconsistent with typeclass
6 participants