Skip to content

Commit 18eb282

Browse files
committed
add recursive nullable flatten
1 parent 00c7029 commit 18eb282

File tree

2 files changed

+38
-4
lines changed

2 files changed

+38
-4
lines changed

core/src/main/scala-3/cats/data/Nullable.scala

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ import scala.quoted.*
2828
opaque type Nullable[+A] = A | Null
2929

3030
object Nullable extends NullableInstances {
31+
type Flattened[A] = A match {
32+
case Nullable[x] => Nullable[x]
33+
case _ => Nullable[A]
34+
}
35+
36+
private[data] sealed trait Flattening[A, B] {
37+
def apply(value: Nullable[A]): B
38+
}
39+
40+
private[data] object Flattening extends Flattening0 {
41+
given [A, B](using next: Flattening[A, B]): Flattening[Nullable[A], B] with {
42+
def apply(value: Nullable[Nullable[A]]): B = {
43+
next(value.asInstanceOf[Nullable[A]])
44+
}
45+
}
46+
}
47+
48+
private[data] trait Flattening0 {
49+
given [A]: Flattening[A, Nullable[A]] with {
50+
def apply(value: Nullable[A]): Nullable[A] = {
51+
value
52+
}
53+
}
54+
}
55+
3156
// If we enable explicit nulls, strict equality needs this `CanEqual` for `== null` checks.
3257
// We keep it `private[data]` (not `private`) because explicit nulls are not enabled right now,
3358
// and a `private given` would otherwise trigger an unused-private-member warning.
@@ -79,11 +104,9 @@ object Nullable extends NullableInstances {
79104
inline def iterator: Iterator[A] = {
80105
fold(Iterator.empty)(Iterator.single(_))
81106
}
82-
}
83107

84-
extension [A](inline nested: Nullable[Nullable[A]]) {
85-
inline def flatten: Nullable[A] = {
86-
nested
108+
inline def flatten[B](using flattening: Flattening[A, B]): B = {
109+
flattening(nullable)
87110
}
88111
}
89112

tests/shared/src/test/scala-3/cats/tests/NullableSuite.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ class NullableSuite extends CatsSuite {
135135
}
136136
}
137137

138+
test("flatten removes any number of nested Nullable wrappers down to one layer") {
139+
val tripleEmpty: Nullable[Nullable[Nullable[Int]]] = Nullable(Nullable(Nullable.empty[Int]))
140+
val tripleNonEmpty: Nullable[Nullable[Nullable[Int]]] = Nullable(Nullable(Nullable(1)))
141+
142+
val flattenedEmpty: Nullable[Int] = tripleEmpty.flatten
143+
val flattenedNonEmpty: Nullable[Int] = tripleNonEmpty.flatten
144+
145+
assert(flattenedEmpty === Nullable.empty[Int])
146+
assert(flattenedNonEmpty === Nullable(1))
147+
}
148+
138149
test("minimal Functor[Nullable] fails composition when composed with itself") {
139150
val candidate = new Functor[Nullable] {
140151
def map[A, B](fa: Nullable[A])(f: A => B): Nullable[B] =

0 commit comments

Comments
 (0)