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

Add take/takeRight/drop/dropRight to Chain #4694

Merged
merged 7 commits into from
Jan 18, 2025

Conversation

johnynek
Copy link
Contributor

@johnynek johnynek commented Jan 3, 2025

I reached for drop on Chain and realized we never implemented it however, we could implement it and using internal structure it can be more efficient than using iterators/toList.

@@ -308,13 +310,14 @@ sealed abstract class Chain[+A] extends ChainCompat[A] {
arg match {
case Wrap(seq) =>
if (count == 1) {
lhs.append(seq.last)
seq.last +: rhs
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was the bug. The rest of the changes are just optimizations to the code or scalacheck improvements.

@satorg
Copy link
Contributor

satorg commented Jan 3, 2025

Thank you, these methods are really nice addition to Chain!

One thing to clarify: Chain uses Long for size. Would it make sense to make count parameter Long as well?
I realize it is unlikely that some one will ever hit Int limits in there, but size: Long implies that it is possible at least in theory.

Comment on lines 278 to 280
if (newCount > 0) {
// we have to keep takeping on the rhs
go(newLhs, newCount, rhs, Chain.nil)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we eagerly exit with the newLhs if rhs.isEmpty == true?

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 don't think so, we have to check for empty anyway on the next call call of the loop. If we also check here we are checking twice. If we could ensure we never see Empty in the loop it would be worth it, but it would mean we would we have uglier code: we don't have a total match, we have one arm (Empty) that is unreachable. I don't think the performance win would be there and it would complicate the code.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, actually, I took this suggestion. I remembered we have NonEmpty so we can recurse on that instead, that naturally pushes the check up to the places you suggest.

I still don't think it's any faster (since the Empty was the final check and always a terminal condition), but at least it's nice to be clear in the recursion that we are only operating on NonEmptyChain.

Comment on lines 321 to 323
if (newCount > 0) {
// we have to keep takeping on the rhs
go(Chain.nil, newCount, lhs, newRhs)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we eagerly exit with the newRhs if lhs.isEmpty == true?

Comment on lines 378 to 380
if (newCount > 0) {
// we have to keep dropping on the rhs
go(newCount, rhs, Chain.nil)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we eagerly exit with the Empty if rhs.isEmpty == true?

danicheg
danicheg previously approved these changes Jan 4, 2025
Comment on lines 394 to 395
// dropped is not empty
val wrapped = Wrap(dropped)
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess dropped can have size == 1 here, but according to the Wrap's invariant:

/*
* Invariant: (seq.length >= 2)
* if the length is zero, fromSeq returns Empty
* if the length is one, fromSeq returns Singleton
*
* The only places we create Wrap is in fromSeq and in methods that preserve
* length: zipWithIndex, map, sort
*/
final private[data] case class Wrap[A](seq: immutable.Seq[A]) extends NonEmpty[A]

So it looks like in order to meet the invariant criteria it should be something like

val wrapped = if (dropped.size > 1) Wrap(dropped) else Singleton(dropped.head)

The same concern is for dropRight and maybe take/takeRight methods.

However, perhaps it would be even better to add a general-purpose method to object Chain that could guarantee the invariant, e.g.

private def wrapOrSingleton[A](seq: Seq[A): NonEmpty[A] =
  if (seq.size > 1) Wrap(seq) else Singleton(seq.head) // assume seq is never empty here

Also, it looks like this note in the docs to Wrap:

The only places we create Wrap is in fromSeq and in methods that preserve length: zipWithIndex, map, sort

is not applicable anymore.

Copy link
Contributor

@satorg satorg Jan 9, 2025

Choose a reason for hiding this comment

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

UPD.: actually, take/takeRight don't seem to be affected. But drop/dropRight do. For example:

Chain.fromSeq(Seq(1, 2)).drop(1)

results to Chain.Wrap(2)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch. I fixed the comment and added the branch. I did use lengthCompare(1) rather than branching on .size since the former is more efficient for List (or other Seqs that don't store the length).

@johnynek
Copy link
Contributor Author

I think the catsNative has been giving spurious failures, so I think this is ready now?

@satorg satorg self-requested a review January 11, 2025 02:00
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!

@satorg
Copy link
Contributor

satorg commented Jan 18, 2025

Hi @johnynek , @danicheg, sorry for the ping, just to clarify with you guys – where do we stand on this?

The reason I'm asking – I'd like to kick off v2.13.0, but I really like this feature and think it would be nice to have it included. However, if there are some concerns or work to-do for this PR, then perhaps it makes sense to release without this one and postpone it for a follow-up release. So WDYT?

@johnynek
Copy link
Contributor Author

I think it's ready. I just didn't want to merge my own PR without the second +1.

@danicheg
Copy link
Member

It's definitely good to go 👍🏼

@johnynek johnynek merged commit 32a50dc into main Jan 18, 2025
33 checks passed
@johnynek
Copy link
Contributor Author

It might be nice to have splitAt also. You can implement it with drop and take but I think you can do a single pass with a better implementation.

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