Skip to content

Prevent Chain instances being backed by mutable or lazy Seq#4169

Merged
armanbilge merged 8 commits intotypelevel:mainfrom
bplommer:fix-chain-from-mut-seq
May 31, 2022
Merged

Prevent Chain instances being backed by mutable or lazy Seq#4169
armanbilge merged 8 commits intotypelevel:mainfrom
bplommer:fix-chain-from-mut-seq

Conversation

@bplommer
Copy link
Copy Markdown
Contributor

@bplommer bplommer commented Apr 8, 2022

This addresses two issues:

  • Changes Chain.Wrap to wrap immutable.Seq instead of Seq, which until 2.13 could be mutable.
  • When converting from IterableOnce, explicitly convert to Vector. Previous use of .iterator.toSeq can return a lazy Seq, creating race conditions - in 2.12 it returns Stream. In 2.13 it returns List, but this isn't guaranteed by the return type.

I haven't attempted to prevent Chain from wrapping a LazyList or Stream passed directly to the constructor.

@bplommer bplommer marked this pull request as ready for review April 8, 2022 10:32
@satorg
Copy link
Copy Markdown
Contributor

satorg commented Apr 8, 2022

Just to clarify: what are the race conditions you mentioned?

case s: ImSeq[A] => fromImmutableSeq(s) // pay O(1) not O(N) cost
case s: MutSeq[A] => fromMutableSeq(s)
case notSeq =>
fromImmutableSeq(notSeq.toVector) // toSeq could return a Stream, creating potential race conditions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let's add a more complete comment here. I don't know the race conditions you are referring to, and future comment readers might also be confused.

I think toVector is fine even if there is no race conditions because Vector is very memory efficient (due to internally using Array).

Copy link
Copy Markdown
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.

Thanks for taking this!

Nice safety fix.

Comment on lines +15 to +18
s match {
case mut: MutSeq[A] => fromMutableSeq(mut)
case imm: ImSeq[A] => fromImmutableSeq(imm)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not an objection but rather just a suggestion about the style.

I personally don't like such renames on imports, because they tend to introduce new not-well known names. Moreover, Scala Docs suggests a convention about it:
https://docs.scala-lang.org/overviews/collections/overview.html

I.e. you could import it in this way:

import scala.collection.immutable
import scala.collection.mutable

Then use it in the code:

s match {
  case seq: mutable.Seq[A] => fromMutableSeq(mut)
  case seq: immutable.Seq[A]  => fromImmutableSeq(imm)
}

That looks way clearer and easier to read, doesn't it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yup, I very much feel the same way about this.

Comment on lines +20 to +24
private def fromImmutableSeq[A](s: ImSeq[A]): Chain[A] = {
if (s.isEmpty) nil
else if (s.lengthCompare(1) == 0) one(s.head)
else Wrap(s)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This implementation is identical in both Scala 2.12 and 2.13+ versions. I wonder – does it make sense to make it shared to avoid unnecessary duplication?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Eh, I don't think it's worth the mental indirection when reading the code. It's just a few lines, it's fine.

def fromSeq[A](s: Seq[A]): Chain[A] =
s match {
case mut: MutSeq[A] => fromMutableSeq(mut)
case imm: ImSeq[A] => fromImmutableSeq(imm)
Copy link
Copy Markdown
Contributor

@johnynek johnynek Apr 8, 2022

Choose a reason for hiding this comment

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

actually, Seq isn't sealed, so in principle there could be other subclasses.

Can we instead do:

s match {
  case imm: ImSeq[A] => fromImmutableSeq(imm)
  case _ =>
    if (s.isEmpty) Empty
    else if (s.lengthCompare(1) == 0) Singleton(s.head)
    else Wrap(s.toVector)
}

@armanbilge
Copy link
Copy Markdown
Member

@bplommer polite bump, seems just a few fixes and then this is mergeable :) thanks!

@armanbilge armanbilge added this to the 2.8.0 milestone May 16, 2022
@armanbilge armanbilge linked an issue May 17, 2022 that may be closed by this pull request
@bplommer
Copy link
Copy Markdown
Contributor Author

@bplommer polite bump, seems just a few fixes and then this is mergeable :) thanks!

Completely forgot about this! Will do soon :)

@armanbilge armanbilge requested review from armanbilge, johnynek and satorg and removed request for armanbilge May 28, 2022 15:02
@armanbilge armanbilge merged commit a5ade30 into typelevel:main May 31, 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.

Chain in Cats built for Scala 2.12 can keep a mutable collection

4 participants