Skip to content

Commit fb3c17e

Browse files
add lazy withFilter() method for efficient compilation of for-comprehensions with if guard
1 parent bc21eb0 commit fb3c17e

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

core/src/main/scala/cats/FunctorFilter.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ trait FunctorFilter[F[_]] extends Serializable {
9797
}
9898

9999
object FunctorFilter extends ScalaVersionSpecificTraverseFilterInstances with FunctorFilterInstances0 {
100+
101+
/**
102+
* A lazy wrapper supporting Scala `for`-comprehensions.
103+
*/
104+
class WithFilter[F[_], A](fa: F[A], p: A => Boolean)(implicit F: FunctorFilter[F]) {
105+
def map[B](f: A => B): F[B] =
106+
F.mapFilter(fa)(a => if (p(a)) Some(f(a)) else None)
107+
108+
def flatMap[B](f: A => F[B])(implicit flatMapF: FlatMap[F]): F[B] =
109+
flatMapF.flatMap(F.filter(fa)(p))(f)
110+
111+
def withFilter(q: A => Boolean): WithFilter[F, A] =
112+
new WithFilter[F, A](fa, a => p(a) && q(a))
113+
}
100114
implicit def catsTraverseFilterForOption: TraverseFilter[Option] =
101115
cats.instances.option.catsStdTraverseFilterForOption
102116
implicit def catsTraverseFilterForList: TraverseFilter[List] = cats.instances.list.catsStdTraverseFilterForList
@@ -135,6 +149,8 @@ object FunctorFilter extends ScalaVersionSpecificTraverseFilterInstances with Fu
135149
typeClassInstance.flattenOption[B](self.asInstanceOf[F[Option[B]]])
136150
def filter(f: A => Boolean): F[A] = typeClassInstance.filter[A](self)(f)
137151
def filterNot(f: A => Boolean): F[A] = typeClassInstance.filterNot[A](self)(f)
152+
def withFilter(f: A => Boolean): FunctorFilter.WithFilter[F, A] =
153+
new FunctorFilter.WithFilter[F, A](self, f)(typeClassInstance)
138154
}
139155
trait AllOps[F[_], A] extends Ops[F, A]
140156
trait ToFunctorFilterOps extends Serializable {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2015 Typelevel
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package cats.tests
23+
24+
import cats.syntax.all.*
25+
import cats.Functor
26+
import cats.FunctorFilter
27+
28+
class FunctorFilterSuite extends CatsSuite {
29+
30+
test("withFilter alias allows for-comprehensions with guards") {
31+
// Explicitly use FunctorFilter to provide the withFilter method
32+
// to prove that for-comprehension guards work on any FunctorFilter
33+
def filterEvens[F[_]: FunctorFilter, A](fa: F[A])(implicit
34+
ev: A =:= Int
35+
): F[A] = {
36+
implicit val F: Functor[F] = FunctorFilter[F].functor
37+
for {
38+
a <- fa
39+
if ev(a) % 2 == 0
40+
} yield a
41+
}
42+
43+
val list = List(1, 2, 3, 4, 5)
44+
val evens = filterEvens(list)
45+
46+
assertEquals(evens, List(2, 4))
47+
}
48+
49+
test("withFilter is lazy and does not evaluate until map is called") {
50+
var evaluationCount = 0
51+
52+
val list = List(1, 2, 3)
53+
54+
def getWrapper[F[_]: FunctorFilter, A](fa: F[A])(f: A => Boolean) =
55+
fa.withFilter(f)
56+
57+
// Create the lazy WithFilter wrapper.
58+
// If it were strict, it would iterate immediately.
59+
val lazyWrapper = getWrapper(list) { x =>
60+
evaluationCount += 1
61+
x > 1
62+
}
63+
64+
// It has not been mapped yet, so the evaluation count should be 0.
65+
assertEquals(evaluationCount, 0)
66+
67+
// Now we map over it. This forces the evaluation.
68+
val result = lazyWrapper.map(_ * 2)
69+
70+
// The list has 3 elements, so the predicate should be called 3 times.
71+
assertEquals(evaluationCount, 3)
72+
assertEquals(result, List(4, 6))
73+
}
74+
75+
}

0 commit comments

Comments
 (0)