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

Avoid double evaluation of pattern matchers in Chunk.collect #3364

Merged
merged 1 commit into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions benchmark/src/main/scala/fs2/benchmark/ChunkBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,14 @@ class ChunkBenchmark {
ints.filter(_ % 3 == 0)
()
}

private object OddStringExtractor {
def unapply(i: Int): Option[String] = if (i % 2 != 0) Some(i.toString) else None
}

@Benchmark
def collect(): Unit = {
ints.collect { case OddStringExtractor(s) => s }
()
}
}
3 changes: 2 additions & 1 deletion core/shared/src/main/scala/fs2/Chunk.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ abstract class Chunk[+O] extends Serializable with ChunkPlatform[O] with ChunkRu
def collect[O2](pf: PartialFunction[O, O2]): Chunk[O2] = {
val b = makeArrayBuilder[Any]
b.sizeHint(size)
foreach(o => if (pf.isDefinedAt(o)) b += pf(o))
val f = pf.runWith(b += _)
foreach { o => f(o); () }
Copy link
Contributor Author

@flipp5b flipp5b Dec 20, 2023

Choose a reason for hiding this comment

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

Well, this looks rather ugly. pf.runWith returns O => Boolean so we have to transform it to O => Unit to avoid warnings. This could be rewritten as follows (at the cost of extra lambda call per Chunk item):

foreach(pf.runWith(b += _).andThen(_ => ()))

@armanbilge, which one would you prefer?

Copy link
Member

Choose a reason for hiding this comment

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

Ugly but performant is the right choice 😅

Chunk.array(b.result()).asInstanceOf[Chunk[O2]]
}

Expand Down
31 changes: 31 additions & 0 deletions core/shared/src/test/scala/fs2/ChunkSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import scodec.bits.ByteVector

import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.util.concurrent.atomic.AtomicInteger
import scala.reflect.ClassTag

class ChunkSuite extends Fs2Suite {
Expand Down Expand Up @@ -108,6 +109,36 @@ class ChunkSuite extends Fs2Suite {
assert(Chunk.javaList(c.asJava) eq c)
}
}

test("Chunk.collect behaves as filter + map") {
forAll { (c: Chunk[Int]) =>
val extractor = new OddStringExtractor
val pf: PartialFunction[Int, String] = { case extractor(s) => s }

val result = c.collect(pf)

assertEquals(result, c.filter(pf.isDefinedAt).map(pf))
}
}

test("Chunk.collect evaluates pattern matchers once per item") {
forAll { (c: Chunk[Int]) =>
val extractor = new OddStringExtractor

val _ = c.collect { case extractor(s) => s }

assertEquals(extractor.callCounter.get(), c.size)
}
}

class OddStringExtractor {
val callCounter: AtomicInteger = new AtomicInteger(0)

def unapply(i: Int): Option[String] = {
callCounter.incrementAndGet()
if (i % 2 != 0) Some(i.toString) else None
}
}
}

def testChunk[A: Arbitrary: ClassTag: CommutativeMonoid: Eq: Cogen](
Expand Down