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

Relax a constraint for Alternative's unite and separate methods from Monad to FlatMap #3997

Merged

Conversation

satorg
Copy link
Contributor

@satorg satorg commented Sep 19, 2021

The initial discussion is here.

Rationale

  • The unite and separate methods don't need a Monad actually to do their job, because they only use flatMap in addition to methods from Alternative. Therefore the FlatMap[F] constraint is just enough there.
  • Moreover, both Alternative and Monad implement Applicative. That means the Applicative methods can come by two different paths which creates sort of a loophole for ambiguity:
    implicit val fooMonad: Monad[Foo] = ??? // assume Monad[Foo] is different from Alternative[Foo]
    Alternative[Foo].unite[Bar, Int](Foo(Bar(123)) // which `pure` should it use – one from `Alternative` or `Monad` ? 

Discussion

In fact, if it is okay to constraint the unite and separate methods with a Monad, then the Alternative typeclass is absolutely redundant in such a case – we can simply put these two methods directly into MonoidK.

Another approach (perhaps even better one) could be a new typeclass (let's name it Monadditive for the sake of this example) which would inherit to Monad and MonoidK (via Alternative) instead of just Applicative and MonoidK (which the current Alternative inherits to). In this case the definition of those two methods – unite and separate – could be simplified to the following:

trait Monadditive[F[_]] extends Alternative[F] with Monad[F] { // implies `MonoidK` as well
  def unite[G[_], A](fga: F[G[A]])(implicit G: Foldable[G]): F[A] = ??? // implementation remains the same
  def separate[G[_, _], A, B](fgab: F[G[A, B]])(implicit G: Bifoldable[G]): (F[A], F[B]) = ??? // implementation remains the same
}

In other words, an additional constraint for F[_] is not necessary anymore.

Therefore such a new typeclass would allow to reduce number of additional constraints passed to those methods.
And looks like it would make a lot of sense anyway – most of the containers which Cats supports implement corresponding Monad instances, not only Applicative.

@satorg satorg marked this pull request as draft September 19, 2021 21:44
@satorg
Copy link
Contributor Author

satorg commented Sep 19, 2021

Converting the PR to Draft since MiMa is not happy with it:

[error] (coreJVM / mimaReportBinaryIssues) Failed binary compatibility check against org.typelevel:cats-core_2.13:2.6.1! Found 1 potential problems

Let me investigate the issue first. If it is not worth the effort to fix it, I'll close the PR.

@rossabaker
Copy link
Member

If you keep the old one and make it private[cats], you'll fix that MiMa complaint. Then you'll be fighting ambiguous overloads at call sites within cats, and cats-rooted projects like cats-effect. You can fix the ambiguous overload by putting the old one in a bincompat parent trait, but then a different MiMa error pops up. We've done this successfully on instances, but I don't remember a precedent on typeclasses.

There's also syntax to wrestle with.

@satorg
Copy link
Contributor Author

satorg commented Sep 22, 2021

@rossabaker thank you for the advice. As I can see now it won't be an easy lift. Seems I managed to calm MiMa down at this point, although the solution I came up with looks quite ugly to me. As you can see in the last commit, I added a new AlternativeBinCompat0 trait for that. Initially I tried to move the old unite method to this trait, but it didn't make MiMa happy unfortunately. It simply changed the way it complaints to

[error] Cats core: Failed binary compatibility check against org.typelevel:cats-core_2.13:2.6.1! Found 1 potential problems
[error]  * in current version, classes mixing cats.Alternative need be recompiled to wire to the new static mixin forwarder method all super calls to method unite(java.lang.Object,cats.Monad,cats.Foldable)java.lang.Object
[error]    filter with: ProblemFilters.exclude[NewMixinForwarderProblem]("cats.Alternative.unite")

So I decided to try the opposite: place the new unite into AlternativeBinCompat0 while leaving the old one in place (but restricting its visibility via private[cats]. Currently all checks pass, but I haven't got started with the syntax yet.

@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch from 4a68a42 to 4bc84d7 Compare October 4, 2021 07:17
@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch 3 times, most recently from ae92c24 to 25da570 Compare November 8, 2021 20:06
@satorg satorg changed the title Relax a constraint for Alternative.unite method Relax a constraint for Alternative's unite and separate methods from Monad to FlatMap Nov 8, 2021
@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch from 25da570 to 834e720 Compare November 9, 2021 08:26
@satorg satorg marked this pull request as ready for review November 9, 2021 08:58
@satorg
Copy link
Contributor Author

satorg commented Nov 9, 2021

Seems I managed to get through all the thorns with this task. The only caveat: Simulacrum Scalafix rules don't work correctly with all these binary compatibility tricks in Alternative I had to apply. But that Simulacrum generated syntax is not currently in use for Alternative anyway.

Comment on lines +32 to +33
def unite[G[_], A](fga: F[G[A]])(implicit FM: FlatMap[F], G: Foldable[G]): F[A] =
FM.flatMap(fga) { G.foldMapK(_)(pure)(self) }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The original implementation is replaced with this better optimized one which utilizes Foldable.foldMapK (instead of the former's foldLeft).

@satorg
Copy link
Contributor Author

satorg commented Nov 9, 2021

cc @rossabaker, in case if you're curious in how I've fought it :)

@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch from 834e720 to c9f8c17 Compare November 15, 2021 08:34
@rossabaker rossabaker self-requested a review November 17, 2021 05:25
@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch from c9f8c17 to 19f8982 Compare November 18, 2021 20:01
@satorg
Copy link
Contributor Author

satorg commented Nov 19, 2021

Hmm.. Seems the build failure is not related to the PR:

[error] org.scalajs.ir.IRVersionNotSupportedException: Failed to deserialize a file compiled with Scala.js 1.7 (supported up to: 1.5): /home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-js/scalajs-library_2.13/1.7.1/scalajs-library_2.13-1.7.1.jar:/scala/volatile.sjsir
[error] You may need to upgrade the Scala.js sbt plugin to version 1.7 or later.
[error] 	at org.scalajs.sbtplugin.ScalaJSPluginInternal$.enhanceIRVersionNotSupportedException(ScalaJSPluginInternal.scala:83)
[error] 	at org.scalajs.sbtplugin.ScalaJSPluginInternal$.$anonfun$scalaJSStageSettings$19(ScalaJSPluginInternal.scala:227)

I wonder – is the main branch broken (how come?)

@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch 2 times, most recently from 696864f to f22264e Compare November 19, 2021 03:02
@rossabaker
Copy link
Member

Yes, that scala.js issue came up on another branch. Pretty sure it's a result of #4018.

@rossabaker
Copy link
Member

#4048 should fix the build.

Copy link
Member

@rossabaker rossabaker left a comment

Choose a reason for hiding this comment

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

Nice work! I've used package private to hide overloads, but can't recall ever using a protected. It seems to work. Have you tried calling it anywhere with something that has a FlatMap but no Monad? I'm mostly sure it will still work, but want to be 100% sure. (Maybe this encoding isn't as novel as I think.)

core/src/main/scala/cats/syntax/alternative.scala Outdated Show resolved Hide resolved
@satorg
Copy link
Contributor Author

satorg commented Nov 19, 2021

I've used package private to hide overloads, but can't recall ever using a protected. It seems to work.

Right, good catch. First I tried to make it private[cats] indeed. And it works in terms of binary compatibility too. But it makes testing of the new methods a way more sophisticated since apparently all the tests are also located inside the cats package and the compiler tries to route all the calls to unite and separate to the old methods instead of the new ones. Using protected instead of private[cats] basically solves the problem while preserving the binary compatibility.

Interesting observation: this trick with protected does not work for syntax – it breaks BC for Scala 2.12 (but still works for Scala 2.13 and Scala 3). I don't know exactly why.

@rossabaker
Copy link
Member

Interesting observation: this trick with protected does not work for syntax – it breaks BC for Scala 2.12

Oh, so this is still going to fail MiMa even after #4048 is merged? 🤔

@satorg
Copy link
Contributor Author

satorg commented Nov 19, 2021

Oh, so this is still going to fail MiMa even after #4048 is merged? 🤔

No, I don't think so. I mean, all the checks had been succeeding before #4018. But later I rebased this PR onto main to test it against the latest version – and the checks began failing. I'm going to make the changes you requested and we will see what happens anyway :)

@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch from f22264e to 51051ca Compare November 19, 2021 08:59
@satorg
Copy link
Contributor Author

satorg commented Nov 19, 2021

Have you tried calling it anywhere with something that has a FlatMap but no Monad?

Good catch, thanks. Seems I overlooked that it has to be explicitly tested. Should be good now.

rossabaker
rossabaker previously approved these changes Nov 19, 2021
rossabaker
rossabaker previously approved these changes Nov 22, 2021
@satorg
Copy link
Contributor Author

satorg commented Nov 22, 2021

@rossabaker thank you for keeping eye on this PR and helping me with polishing it, I appreciate it.

Yeah, as you might notice, I eventually decided to revert access for UniteOps and SeparateOps constructors back to public. Although I still believe it would be better to make them private[syntax], but the improvement would be so minutiae that it wouldn't be worth the compatibility risk it imposes.

@satorg satorg force-pushed the relax-alternative-unite-method-constraints branch from 5cd2beb to 2f10dec Compare November 22, 2021 23:10
@satorg
Copy link
Contributor Author

satorg commented Nov 22, 2021

Ironically, this PR had got conflicts with another my PR that have been merged recently.

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.

3 participants