Skip to content

Commit 9db3b7e

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

2 files changed

Lines changed: 163 additions & 65 deletions

File tree

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

Lines changed: 88 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -23,83 +23,95 @@ package cats
2323

2424
import scala.collection.immutable.{Queue, Seq, SortedMap}
2525

26-
/**
27-
* `FunctorFilter[F]` allows you to `map` and filter out elements simultaneously.
28-
*/
26+
/** `FunctorFilter[F]` allows you to `map` and filter out elements
27+
* simultaneously.
28+
*/
2929

3030
trait FunctorFilter[F[_]] extends Serializable {
3131
def functor: Functor[F]
3232

33-
/**
34-
* A combined `map` and `filter`. Filtering is handled via `Option`
35-
* instead of `Boolean` such that the output type `B` can be different than
36-
* the input type `A`.
37-
*
38-
* Example:
39-
* {{{
40-
* scala> import cats.syntax.all._
41-
* scala> val m: Map[Int, String] = Map(1 -> "one", 3 -> "three")
42-
* scala> val l: List[Int] = List(1, 2, 3, 4)
43-
* scala> def asString(i: Int): Option[String] = m.get(i)
44-
* scala> l.mapFilter(asString)
45-
* res0: List[String] = List(one, three)
46-
* }}}
47-
*/
33+
/** A combined `map` and `filter`. Filtering is handled via `Option` instead
34+
* of `Boolean` such that the output type `B` can be different than the input
35+
* type `A`.
36+
*
37+
* Example:
38+
* {{{
39+
* scala> import cats.syntax.all._
40+
* scala> val m: Map[Int, String] = Map(1 -> "one", 3 -> "three")
41+
* scala> val l: List[Int] = List(1, 2, 3, 4)
42+
* scala> def asString(i: Int): Option[String] = m.get(i)
43+
* scala> l.mapFilter(asString)
44+
* res0: List[String] = List(one, three)
45+
* }}}
46+
*/
4847
def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B]
4948

50-
/**
51-
* Similar to [[mapFilter]] but uses a partial function instead of a function
52-
* that returns an `Option`.
53-
*
54-
* Example:
55-
* {{{
56-
* scala> import cats.syntax.all._
57-
* scala> val l: List[Int] = List(1, 2, 3, 4)
58-
* scala> FunctorFilter[List].collect(l){
59-
* | case 1 => "one"
60-
* | case 3 => "three"
61-
* | }
62-
* res0: List[String] = List(one, three)
63-
* }}}
64-
*/
49+
/** Similar to [[mapFilter]] but uses a partial function instead of a function
50+
* that returns an `Option`.
51+
*
52+
* Example:
53+
* {{{
54+
* scala> import cats.syntax.all._
55+
* scala> val l: List[Int] = List(1, 2, 3, 4)
56+
* scala> FunctorFilter[List].collect(l){
57+
* | case 1 => "one"
58+
* | case 3 => "three"
59+
* | }
60+
* res0: List[String] = List(one, three)
61+
* }}}
62+
*/
6563
def collect[A, B](fa: F[A])(f: PartialFunction[A, B]): F[B] =
6664
mapFilter(fa)(f.lift)
6765

68-
/**
69-
* "Flatten" out a structure by collapsing `Option`s.
70-
* Equivalent to using `mapFilter` with `identity`.
71-
*
72-
* Example:
73-
* {{{
74-
* scala> import cats.syntax.all._
75-
* scala> val l: List[Option[Int]] = List(Some(1), None, Some(3), None)
76-
* scala> l.flattenOption
77-
* res0: List[Int] = List(1, 3)
78-
* }}}
79-
*/
66+
/** "Flatten" out a structure by collapsing `Option`s. Equivalent to using
67+
* `mapFilter` with `identity`.
68+
*
69+
* Example:
70+
* {{{
71+
* scala> import cats.syntax.all._
72+
* scala> val l: List[Option[Int]] = List(Some(1), None, Some(3), None)
73+
* scala> l.flattenOption
74+
* res0: List[Int] = List(1, 3)
75+
* }}}
76+
*/
8077
def flattenOption[A](fa: F[Option[A]]): F[A] =
8178
mapFilter(fa)(identity)
8279

83-
/**
84-
* Apply a filter to a structure such that the output structure contains all
85-
* `A` elements in the input structure that satisfy the predicate `f` but none
86-
* that don't.
87-
*/
80+
/** Apply a filter to a structure such that the output structure contains all
81+
* `A` elements in the input structure that satisfy the predicate `f` but
82+
* none that don't.
83+
*/
8884
def filter[A](fa: F[A])(f: A => Boolean): F[A] =
8985
mapFilter(fa)(a => if (f(a)) Some(a) else None)
9086

91-
/**
92-
* Apply a filter to a structure such that the output structure contains all
93-
* `A` elements in the input structure that do not satisfy the predicate `f`.
94-
*/
87+
/** Apply a filter to a structure such that the output structure contains all
88+
* `A` elements in the input structure that do not satisfy the predicate `f`.
89+
*/
9590
def filterNot[A](fa: F[A])(f: A => Boolean): F[A] =
9691
mapFilter(fa)(Some(_).filterNot(f))
9792
}
9893

9994
object FunctorFilter extends ScalaVersionSpecificTraverseFilterInstances with FunctorFilterInstances0 {
95+
96+
/** A lazy wrapper supporting Scala `for`-comprehensions.
97+
*/
98+
class WithFilter[F[_], A](fa: F[A], p: A => Boolean)(implicit
99+
F: FunctorFilter[F]
100+
) {
101+
def map[B](f: A => B): F[B] =
102+
F.mapFilter(fa)(a => if (p(a)) Some(f(a)) else None)
103+
104+
def flatMap[B](f: A => F[B])(implicit flatMapF: FlatMap[F]): F[B] =
105+
flatMapF.flatMap(F.filter(fa)(p))(f)
106+
107+
def withFilter(q: A => Boolean): WithFilter[F, A] =
108+
new WithFilter[F, A](fa, a => p(a) && q(a))
109+
}
110+
100111
implicit def catsTraverseFilterForOption: TraverseFilter[Option] =
101112
cats.instances.option.catsStdTraverseFilterForOption
102-
implicit def catsTraverseFilterForList: TraverseFilter[List] = cats.instances.list.catsStdTraverseFilterForList
113+
implicit def catsTraverseFilterForList: TraverseFilter[List] =
114+
cats.instances.list.catsStdTraverseFilterForList
103115
implicit def catsTraverseFilterForVector: TraverseFilter[Vector] =
104116
cats.instances.vector.catsStdTraverseFilterForVector
105117
implicit def catsFunctorFilterForMap[K]: FunctorFilter[Map[K, *]] =
@@ -109,14 +121,17 @@ object FunctorFilter extends ScalaVersionSpecificTraverseFilterInstances with Fu
109121
implicit def catsTraverseFilterForQueue: TraverseFilter[Queue] =
110122
cats.instances.queue.catsStdTraverseFilterForQueue
111123

112-
/**
113-
* Summon an instance of [[FunctorFilter]] for `F`.
114-
*/
115-
@inline def apply[F[_]](implicit instance: FunctorFilter[F]): FunctorFilter[F] = instance
124+
/** Summon an instance of [[FunctorFilter]] for `F`.
125+
*/
126+
@inline def apply[F[_]](implicit
127+
instance: FunctorFilter[F]
128+
): FunctorFilter[F] = instance
116129

117130
@deprecated("Use cats.syntax object imports", "2.2.0")
118131
object ops {
119-
implicit def toAllFunctorFilterOps[F[_], A](target: F[A])(implicit tc: FunctorFilter[F]): AllOps[F, A] {
132+
implicit def toAllFunctorFilterOps[F[_], A](
133+
target: F[A]
134+
)(implicit tc: FunctorFilter[F]): AllOps[F, A] {
120135
type TypeClassType = FunctorFilter[F]
121136
} =
122137
new AllOps[F, A] {
@@ -129,16 +144,23 @@ object FunctorFilter extends ScalaVersionSpecificTraverseFilterInstances with Fu
129144
type TypeClassType <: FunctorFilter[F]
130145
def self: F[A]
131146
val typeClassInstance: TypeClassType
132-
def mapFilter[B](f: A => Option[B]): F[B] = typeClassInstance.mapFilter[A, B](self)(f)
133-
def collect[B](f: PartialFunction[A, B]): F[B] = typeClassInstance.collect[A, B](self)(f)
147+
def mapFilter[B](f: A => Option[B]): F[B] =
148+
typeClassInstance.mapFilter[A, B](self)(f)
149+
def collect[B](f: PartialFunction[A, B]): F[B] =
150+
typeClassInstance.collect[A, B](self)(f)
134151
def flattenOption[B](implicit ev$1: A <:< Option[B]): F[B] =
135152
typeClassInstance.flattenOption[B](self.asInstanceOf[F[Option[B]]])
136153
def filter(f: A => Boolean): F[A] = typeClassInstance.filter[A](self)(f)
137-
def filterNot(f: A => Boolean): F[A] = typeClassInstance.filterNot[A](self)(f)
154+
def filterNot(f: A => Boolean): F[A] =
155+
typeClassInstance.filterNot[A](self)(f)
156+
def withFilter(f: A => Boolean): FunctorFilter.WithFilter[F, A] =
157+
new FunctorFilter.WithFilter[F, A](self, f)(typeClassInstance)
138158
}
139159
trait AllOps[F[_], A] extends Ops[F, A]
140160
trait ToFunctorFilterOps extends Serializable {
141-
implicit def toFunctorFilterOps[F[_], A](target: F[A])(implicit tc: FunctorFilter[F]): Ops[F, A] {
161+
implicit def toFunctorFilterOps[F[_], A](
162+
target: F[A]
163+
)(implicit tc: FunctorFilter[F]): Ops[F, A] {
142164
type TypeClassType = FunctorFilter[F]
143165
} =
144166
new Ops[F, A] {
@@ -154,6 +176,7 @@ object FunctorFilter extends ScalaVersionSpecificTraverseFilterInstances with Fu
154176

155177
trait FunctorFilterInstances0 {
156178

157-
implicit def catsTraverseFilterForSeq: TraverseFilter[Seq] = cats.instances.seq.catsStdTraverseFilterForSeq
179+
implicit def catsTraverseFilterForSeq: TraverseFilter[Seq] =
180+
cats.instances.seq.catsStdTraverseFilterForSeq
158181

159182
}
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)