Skip to content

Weaken some Alternative constraints in OneAnd #4739

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

kory33
Copy link

@kory33 kory33 commented Apr 20, 2025

Currently, the unwrap and combine methods in OneAnd, as well as SemigroupK / Semigroup instance have Alternative as a bound for the type F[_] of collection-like component.

This pull request weakens this constraint to F[_]: NonEmptyAlternative.

Context

The change allows one to unwrap OneOf[NonEmptyList, A] into NonEmptyList[A]. At first glance putting NonEmptyList in OneOf sounds a bit weird, but I think they are natural choices for situations when one wants to describe a collection of 2..N items (this occurred to me while describing "duplicate inputs error" in a validation logic, where the error case class had a length ≥2 list of input indices containing duplicates).

I think this weakening generalization makes sense since, intuitively, none of the aforementioned methods interact with the empty: F[Nothing] aspect of the collection: they only combine collections (SemigroupK) or inject an element into the collection (NonEmptyAlternative).

@@ -123,8 +123,21 @@ final case class OneAnd[F[_], A](head: A, tail: F[A]) {
s"OneAnd(${A.show(head)}, ${FA.show(tail)})"
}

private[data] trait OneAndBinCompat0[F[_], A] {
Copy link
Author

Choose a reason for hiding this comment

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

[question] Is this the right way to pass the MiMa check for a case class modification?

I saw #3997 (comment) and thought that this was the (only) way, but I could not find any other place that defines a similar bincompat trait (other than ValidatedFunctionsBinCompat0, which patches the companion rather than the Validated class itself).

Copy link
Contributor

Choose a reason for hiding this comment

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

can we make this sealed? I think so.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, I made it sealed : af23366

val head: A
val tail: F[A]

@deprecated("Kept for binary compatibility", "2.14.0")
Copy link
Author

Choose a reason for hiding this comment

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

[question] Is since = "2.14.0" specification appropriate (is that the next cats version)? Or do I just not need @deprecated at all since it's private[data]?

Copy link
Contributor

Choose a reason for hiding this comment

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

personally I wouldn't add the deprecated and just add a comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

If a method is superceded by some other method with the same name, we usually not only make it package local, but also remove all implicit keywords from its definition, e.g.:

private[data] def combine(other: OneAnd[F, A])(F: Alternative[F]): OneAnd[F, A] = ...

which makes it less likely to interfere with the new method while preserving binary compatibility.

I personally don't mind marking it @deprecated but don't have strong opinion on that – it is not supposed to be called anyway.

@kory33
Copy link
Author

kory33 commented Apr 20, 2025

Oh, I thought I made the bincompat check pass but I didn't! I'll try to fix it.

johnynek
johnynek previously approved these changes May 7, 2025
val head: A
val tail: F[A]

@deprecated("Kept for binary compatibility", "2.14.0")
Copy link
Contributor

Choose a reason for hiding this comment

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

personally I wouldn't add the deprecated and just add a comment.

@@ -123,8 +123,21 @@ final case class OneAnd[F[_], A](head: A, tail: F[A]) {
s"OneAnd(${A.show(head)}, ${FA.show(tail)})"
}

private[data] trait OneAndBinCompat0[F[_], A] {
Copy link
Contributor

Choose a reason for hiding this comment

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

can we make this sealed? I think so.

Comment on lines 127 to 128
val head: A
val tail: F[A]
Copy link
Contributor

@satorg satorg May 7, 2025

Choose a reason for hiding this comment

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

Abstract vals look suspicious a bit. And might not be necessary – since OneAndBinCompat0 is supposed to be extended by OneAnd only, a trick with self-type annotation might work here:

private[data] trait OneAndBinCompat0[F[_], A] {
  self: OneAnd[F, A] => // allow access to `head` and `tail` from withing this trait
    // no abstract `head` and `tail` should be necessary

Copy link
Author

Choose a reason for hiding this comment

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

For some reason I've never used a non-trait type for self-type annotation and couldn't come up with this!
Applied: af23366

Comment on lines 128 to 133
private[data] def unwrap(implicit F: Alternative[F]): F[A] =
F.prependK(head, tail)

// Kept for binary compatibility
private[data] def combine(other: OneAnd[F, A])(implicit F: Alternative[F]): OneAnd[F, A] =
OneAnd(head, F.combineK(tail, other.unwrap))
Copy link
Contributor

Choose a reason for hiding this comment

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

While not a requirement, but since there's self: OneAnd in the scope and because Alternative extends NonEmptyAlternative, I wonder if it could be simplified even further:

Suggested change
private[data] def unwrap(implicit F: Alternative[F]): F[A] =
F.prependK(head, tail)
// Kept for binary compatibility
private[data] def combine(other: OneAnd[F, A])(implicit F: Alternative[F]): OneAnd[F, A] =
OneAnd(head, F.combineK(tail, other.unwrap))
private[data] def unwrap(implicit F: Alternative[F]): F[A] =
self.unwrap(F)
// Kept for binary compatibility
private[data] def combine(other: OneAnd[F, A])(F: Alternative[F]): OneAnd[F, A] =
self.combine(other)(F)

Also, I would still recommend to go without implicit parameters here because even though these methods are private[data] they are still visible to Cats' internals and may cause unexpected unexpecties in the future. Not a requirement either, though.

Copy link
Author

Choose a reason for hiding this comment

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

That makes sense. I think the same applies to OneAndInstancesBinCompat0 as well, so I made a similar change to the instances trait too (e059ed8).

Copy link
Contributor

@satorg satorg left a comment

Choose a reason for hiding this comment

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

Thank you!

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