Skip to content

Commit 13319e8

Browse files
authored
Merge pull request #255 from kategory/toni-traversefilter
Add TraverseFilter
2 parents c5d26d6 + dc7be24 commit 13319e8

File tree

11 files changed

+169
-25
lines changed

11 files changed

+169
-25
lines changed

kategory-core/src/main/kotlin/kategory/data/Const.kt

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ fun <A, T> ConstKind<A, T>.value(): A = this.ev().value
99

1010
fun <F, U> traverse(f: (T) -> HK<F, U>, FA: Applicative<F>): HK<F, Const<A, U>> = FA.pure(retag())
1111

12+
fun <F, U> traverseFilter(f: (T) -> HK<F, Option<U>>, FA: Applicative<F>): HK<F, Const<A, U>> = FA.pure(retag())
13+
1214
companion object {
1315
fun <T, A> pure(a: A): Const<A, T> = Const(a)
1416

@@ -18,6 +20,9 @@ fun <A, T> ConstKind<A, T>.value(): A = this.ev().value
1820
inline fun <reified A> traverse(MA: Monoid<A> = kategory.monoid<A>()): Traverse<ConstKindPartial<A>> =
1921
ConstTraverseInstanceImplicits.instance()
2022

23+
inline fun <reified A> traverseFilter(MA: Monoid<A> = kategory.monoid<A>()): TraverseFilter<ConstKindPartial<A>> =
24+
ConstTraverseFilterInstanceImplicits.instance()
25+
2126
inline fun <reified A, T> semigroup(SA: Semigroup<A> = kategory.semigroup<A>()): Semigroup<ConstKind<A, T>> =
2227
ConstSemigroupInstanceImplicits.instance(SA)
2328

kategory-core/src/main/kotlin/kategory/data/Option.kt

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package kategory
1313
Monad::class,
1414
Foldable::class,
1515
Traverse::class,
16+
TraverseFilter::class,
1617
MonadFilter::class)
1718
sealed class Option<out A> : OptionKind<A> {
1819

@@ -124,6 +125,14 @@ sealed class Option<out A> : OptionKind<A> {
124125
}
125126
}
126127

128+
fun <G, B> traverseFilter(f: (A) -> HK<G, Option<B>>, GA: Applicative<G>): HK<G, Option<B>> =
129+
this.ev().let { option ->
130+
when (option) {
131+
is Option.Some -> f(option.value)
132+
is Option.None -> GA.pure(Option.None)
133+
}
134+
}
135+
127136
/**
128137
* Returns this $option if it is nonempty '''and''' applying the predicate $p to
129138
* this $option's value returns true. Otherwise, return $none.

kategory-core/src/main/kotlin/kategory/data/OptionT.kt

+9-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ package kategory
4444
inline fun <reified F> foldable(FFF: Foldable<F> = kategory.foldable<F>()): OptionTFoldableInstance<F> =
4545
OptionTFoldableInstanceImplicits.instance(FFF)
4646

47+
inline fun <reified F> traverseFilter(TF: TraverseFilter<F> = kategory.traverseFilter<F>()): OptionTTraverseFilterInstance<F> =
48+
OptionTTraverseFilterInstanceImplicits.instance(TF)
49+
4750
inline fun <reified F> traverse(TF: Traverse<F> = kategory.traverse<F>()): OptionTTraverseInstance<F> =
4851
OptionTTraverseInstanceImplicits.instance(TF)
4952

@@ -62,7 +65,7 @@ package kategory
6265

6366
inline fun <B> cata(crossinline default: () -> B, crossinline f: (A) -> B, FF: Functor<F>): HK<F, B> = fold(default, f, FF)
6467

65-
fun <B> ap(ff: OptionTKind<F, (A) -> B>, MF: Monad<F>): OptionT<F, B> = ff.ev().flatMap ({ f -> map(f, MF) }, MF)
68+
fun <B> ap(ff: OptionTKind<F, (A) -> B>, MF: Monad<F>): OptionT<F, B> = ff.ev().flatMap({ f -> map(f, MF) }, MF)
6669

6770
inline fun <B> flatMap(crossinline f: (A) -> OptionT<F, B>, MF: Monad<F>): OptionT<F, B> = flatMapF({ it -> f(it).value }, MF)
6871

@@ -105,6 +108,11 @@ package kategory
105108

106109
fun <B> foldR(lb: Eval<B>, f: (A, Eval<B>) -> Eval<B>, FF: Foldable<F>): Eval<B> = FF.compose(Option.foldable()).foldRC(value, lb, f)
107110

111+
fun <G, B> traverseFilter(f: (A) -> HK<G, Option<B>>, GA: Applicative<G>, FF: Traverse<F>): HK<G, OptionT<F, B>> {
112+
val fa = ComposedTraverseFilter(FF, Option.traverseFilter(), Option.applicative()).traverseFilterC(value, f, GA)
113+
return GA.map(fa, { OptionT(FF.map(it.unnest(), { it.ev() })) })
114+
}
115+
108116
fun <G, B> traverse(f: (A) -> HK<G, B>, GA: Applicative<G>, FF: Traverse<F>): HK<G, OptionT<F, B>> {
109117
val fa = ComposedTraverse(FF, Option.traverse(), Option.applicative()).traverseC(value, f, GA)
110118
return GA.map(fa, { OptionT(FF.map(it.unnest(), { it.ev() })) })

kategory-core/src/main/kotlin/kategory/instances/ConstInstances.kt

+10
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ object ConstTraverseInstanceImplicits {
5555
@JvmStatic fun <A> instance(): ConstTraverseInstance<A> = object : ConstTraverseInstance<A> {}
5656
}
5757

58+
interface ConstTraverseFilterInstance<X> : ConstTraverseInstance<X>, TraverseFilter<ConstKindPartial<X>> {
59+
60+
override fun <G, A, B> traverseFilter(fa: ConstKind<X, A>, f: (A) -> HK<G, Option<B>>, GA: Applicative<G>): HK<G, ConstKind<X, B>> =
61+
fa.ev().traverseFilter(f, GA)
62+
}
63+
64+
object ConstTraverseFilterInstanceImplicits {
65+
@JvmStatic fun <A> instance(): ConstTraverseFilterInstance<A> = object : ConstTraverseFilterInstance<A> {}
66+
}
67+
5868
interface ConstSemigroup<A, T> : Semigroup<ConstKind<A, T>> {
5969

6070
fun SA(): Semigroup<A>

kategory-core/src/main/kotlin/kategory/instances/OptionTInstances.kt

+22
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,28 @@ object OptionTFoldableInstanceImplicits {
8888
}
8989
}
9090

91+
interface OptionTTraverseFilterInstance<F> :
92+
OptionTTraverseInstance<F>,
93+
TraverseFilter<OptionTKindPartial<F>> {
94+
95+
fun TFF(): TraverseFilter<F>
96+
97+
override fun <G, A, B> traverseFilter(fa: OptionTKind<F, A>, f: (A) -> HK<G, Option<B>>, GA: Applicative<G>): HK<G, OptionT<F, B>> =
98+
fa.ev().traverseFilter(f, GA, TF())
99+
100+
}
101+
102+
object OptionTTraverseFilterInstanceImplicits {
103+
@JvmStatic
104+
fun <F> instance(TF: TraverseFilter<F>): OptionTTraverseFilterInstance<F> = object : OptionTTraverseFilterInstance<F> {
105+
override fun FFF(): Foldable<F> = TF
106+
107+
override fun TF(): Traverse<F> = TF
108+
109+
override fun TFF(): TraverseFilter<F> = TF
110+
}
111+
}
112+
91113
interface OptionTTraverseInstance<F> : OptionTFoldableInstance<F>, Traverse<OptionTKindPartial<F>> {
92114

93115
fun TF(): Traverse<F>

kategory-core/src/main/kotlin/kategory/typeclasses/Composed.kt

+40
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,46 @@ inline fun <F, reified G> Foldable<F>.compose(GT: Foldable<G> = foldable<G>()):
5555
override fun GF(): Foldable<G> = GT
5656
}
5757

58+
interface ComposedTraverseFilter<F, G> :
59+
TraverseFilter<Nested<F, G>>,
60+
ComposedTraverse<F, G> {
61+
62+
override fun FT(): Traverse<F>
63+
64+
override fun GT(): TraverseFilter<G>
65+
66+
override fun GA(): Applicative<G>
67+
68+
override fun <H, A, B> traverseFilter(fa: HK<Nested<F, G>, A>, f: (A) -> HK<H, Option<B>>, HA: Applicative<H>): HK<H, HK<Nested<F, G>, B>> =
69+
HA.map(FT().traverse(fa.unnest(), { ga -> GT().traverseFilter(ga, f, HA) }, HA), { it.nest() })
70+
71+
fun <H, A, B> traverseFilterC(fa: HK<F, HK<G, A>>, f: (A) -> HK<H, Option<B>>, HA: Applicative<H>): HK<H, HK<Nested<F, G>, B>> =
72+
traverseFilter(fa.nest(), f, HA)
73+
74+
companion object {
75+
operator fun <F, G> invoke(
76+
FF: Traverse<F>,
77+
GF: TraverseFilter<G>,
78+
GA: Applicative<G>): ComposedTraverseFilter<F, G> =
79+
object : ComposedTraverseFilter<F, G> {
80+
override fun FT(): Traverse<F> = FF
81+
82+
override fun GT(): TraverseFilter<G> = GF
83+
84+
override fun GA(): Applicative<G> = GA
85+
}
86+
}
87+
}
88+
89+
inline fun <reified F, reified G> TraverseFilter<F>.compose(GT: TraverseFilter<G> = traverseFilter<G>(), GA: Applicative<G> = applicative<G>()):
90+
TraverseFilter<Nested<F, G>> = object : ComposedTraverseFilter<F, G> {
91+
override fun FT(): Traverse<F> = this@compose
92+
93+
override fun GT(): TraverseFilter<G> = GT
94+
95+
override fun GA(): Applicative<G> = GA
96+
}
97+
5898
interface ComposedTraverse<F, G> :
5999
Traverse<Nested<F, G>>,
60100
ComposedFoldable<F, G> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package kategory
2+
3+
import kategory.Option.Some
4+
import kategory.Option.None
5+
6+
interface TraverseFilter<F> : Traverse<F>, FunctorFilter<F>, Typeclass {
7+
8+
fun <G, A, B> traverseFilter(fa: HK<F, A>, f: (A) -> HK<G, Option<B>>, GA: Applicative<G>): HK<G, HK<F, B>>
9+
10+
override fun <A, B> mapFilter(fa: HK<F, A>, f: (A) -> Option<B>): HK<F, B> =
11+
traverseFilter(fa, { Id(f(it)) }, Id.applicative()).value()
12+
13+
fun <G, A> filterA(fa: HK<F, A>, f: (A) -> HK<G, Boolean>, GA: Applicative<G>): HK<G, HK<F, A>> =
14+
traverseFilter(fa, { a -> GA.map(f(a), { b -> if (b) Some(a) else None }) }, GA)
15+
16+
override fun <A> filter(fa: HK<F, A>, f: (A) -> Boolean): HK<F, A> =
17+
filterA(fa, { Id(f(it)) }, Id.applicative()).value()
18+
19+
}
20+
21+
inline fun <reified F, reified G, A, B> HK<F, A>.traverseFilter(
22+
FT: TraverseFilter<F> = traverseFilter<F>(),
23+
GA: Applicative<G> = applicative<G>(),
24+
noinline f: (A) -> HK<G, Option<B>>): HK<G, HK<F, B>> = FT.traverseFilter(this, f, GA)
25+
26+
inline fun <reified F> traverseFilter(): TraverseFilter<F> = instance(InstanceParametrizedType(TraverseFilter::class.java, listOf(typeLiteral<F>())))

kategory-core/src/test/kotlin/kategory/data/ConstTest.kt

+2-17
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,12 @@ class ConstTest : UnitSpec() {
1313
applicative<ConstKindPartial<Int>>() shouldNotBe null
1414
foldable<ConstKindPartial<Int>>() shouldNotBe null
1515
traverse<ConstKindPartial<Int>>() shouldNotBe null
16+
traverseFilter<ConstKindPartial<Int>>() shouldNotBe null
1617
semigroup<ConstKind<Int, Int>>() shouldNotBe null
1718
monoid<ConstKind<Int, Int>>() shouldNotBe null
1819
}
1920

20-
testLaws(TraverseLaws.laws(Const.traverse(IntMonoid), Const.applicative(IntMonoid), { Const(it) }, Eq.any()))
21+
testLaws(TraverseFilterLaws.laws(Const.traverseFilter(IntMonoid), Const.applicative(IntMonoid), { Const(it) }, Eq.any()))
2122
testLaws(ApplicativeLaws.laws(Const.applicative(IntMonoid), Eq.any()))
2223
}
2324
}
24-
25-
fun <A> List<A>?.isNotEmpty(): Boolean =
26-
this != null && this.size > 0
27-
28-
object test {
29-
val x : List<Int>? = null
30-
val xIsNotEmpty: Boolean = x.isNotEmpty()
31-
//false
32-
val y : List<Int>? = emptyList<Int>()
33-
val yIsNotEmpty: Boolean = y.isNotEmpty()
34-
//false
35-
val z : List<Int>? = listOf(1)
36-
val zIsNotEmpty: Boolean = y.isNotEmpty()
37-
//true
38-
}
39-

kategory-core/src/test/kotlin/kategory/data/OptionTTest.kt

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package kategory
22

33
import io.kotlintest.KTestJUnitRunner
4-
import io.kotlintest.matchers.shouldNotBe
54
import io.kotlintest.properties.forAll
5+
import io.kotlintest.matchers.shouldNotBe
66
import org.junit.runner.RunWith
77

88
@RunWith(KTestJUnitRunner::class)
99
class OptionTTest : UnitSpec() {
10-
val EQ_ID: Eq<HK<OptionTKindPartial<IdHK>, Int>> = Eq { a, b ->
10+
11+
fun <A> EQ(): Eq<HK<OptionTKindPartial<A>, Int>> = Eq { a, b ->
12+
a.value() == b.value()
13+
}
14+
15+
fun <A> EQ_NESTED(): Eq<HK<OptionTKindPartial<A>, HK<OptionTKindPartial<A>, Int>>> = Eq { a, b ->
1116
a.value() == b.value()
1217
}
1318

@@ -24,24 +29,31 @@ class OptionTTest : UnitSpec() {
2429
semigroupK<OptionTKindPartial<ListKWHK>>() shouldNotBe null
2530
monoidK<OptionTKindPartial<ListKWHK>>() shouldNotBe null
2631
functorFilter<OptionTKindPartial<ListKWHK>>() shouldNotBe null
32+
traverseFilter<OptionTKindPartial<OptionHK>>() shouldNotBe null
2733
}
2834

2935
testLaws(MonadLaws.laws(OptionT.monad(NonEmptyList.monad()), Eq.any()))
30-
testLaws(TraverseLaws.laws(OptionT.traverse(), OptionT.applicative(Id.monad()), { OptionT(Id(it.some())) }, Eq.any()))
3136
testLaws(SemigroupKLaws.laws(
3237
OptionT.semigroupK(Id.monad()),
3338
OptionT.applicative(Id.monad()),
34-
EQ_ID))
39+
EQ()))
3540

3641
testLaws(MonoidKLaws.laws(
3742
OptionT.monoidK(Id.monad()),
3843
OptionT.applicative(Id.monad()),
39-
EQ_ID))
44+
EQ()))
4045

4146
testLaws(FunctorFilterLaws.laws(
4247
OptionT.functorFilter(),
4348
{ OptionT(Id(it.some())) },
44-
EQ_ID))
49+
EQ()))
50+
51+
testLaws(TraverseFilterLaws.laws(
52+
OptionT.traverseFilter(),
53+
OptionT.applicative(Option.monad()),
54+
{ OptionT(Option(it.some())) },
55+
EQ(),
56+
EQ_NESTED()))
4557

4658
"toLeft for Some should build a correct EitherT" {
4759
forAll { a: Int, b: String ->

kategory-core/src/test/kotlin/kategory/data/OptionTest.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class OptionTest : UnitSpec() {
1919
monad<OptionHK>() shouldNotBe null
2020
foldable<OptionHK>() shouldNotBe null
2121
traverse<OptionHK>() shouldNotBe null
22+
traverseFilter<OptionHK>() shouldNotBe null
2223
semigroup<Option<Int>>() shouldNotBe null
2324
monoid<Option<Int>>() shouldNotBe null
2425
monadError<OptionHK, Unit>() shouldNotBe null
@@ -39,7 +40,7 @@ class OptionTest : UnitSpec() {
3940
}
4041

4142
//testLaws(MonadErrorLaws.laws(monadError<OptionHK, Unit>(), Eq.any(), EQ_EITHER)) TODO reenable once the MonadErrorLaws are parametric to `E`
42-
testLaws(TraverseLaws.laws(Option.traverse(), Option.monad(), ::Some, Eq.any()))
43+
testLaws(TraverseFilterLaws.laws(Option.traverseFilter(), Option.monad(), ::Some, Eq.any()))
4344
testLaws(MonadFilterLaws.laws(Option.monadFilter(), ::Some, Eq.any()))
4445

4546
"fromNullable should work for both null and non-null values of nullable types" {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package kategory
2+
3+
import io.kotlintest.properties.Gen
4+
import io.kotlintest.properties.forAll
5+
import kategory.Option.Some
6+
import kategory.Option.None
7+
8+
object TraverseFilterLaws {
9+
10+
//FIXME(paco): TraverseLaws cannot receive AP::pure due to a crash caused by the inliner. Check in TraverseLaws why.
11+
inline fun <reified F> laws(TF: TraverseFilter<F> = traverseFilter<F>(), GA: Applicative<F> = applicative<F>(), crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<HK<F, Int>>, EQ_NESTED: Eq<HK<F, HK<F, Int>>> = Eq.any()): List<Law> =
12+
TraverseLaws.laws(TF, GA, cf, EQ) + listOf(
13+
Law("TraverseFilter Laws: Identity", { identityTraverseFilter(TF, GA, EQ_NESTED) }),
14+
Law("TraverseFilter Laws: filterA consistent with TraverseFilter", { filterAconsistentWithTraverseFilter(TF, GA, EQ_NESTED) })
15+
)
16+
17+
inline fun <reified F> identityTraverseFilter(FT: TraverseFilter<F>, GA: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, HK<F, Int>>> = Eq.any()) =
18+
forAll(genApplicative(genIntSmall(), GA), { fa: HK<F, Int> ->
19+
FT.traverseFilter(fa, { it.some().pure(GA) }, GA).equalUnderTheLaw(GA.pure(fa), EQ)
20+
})
21+
22+
inline fun <reified F> filterAconsistentWithTraverseFilter(FT: TraverseFilter<F>, GA: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, HK<F, Int>>> = Eq.any()) =
23+
forAll(genApplicative(genIntSmall(), GA), genFunctionAToB(genApplicative(Gen.bool(), GA)), { fa: HK<F, Int>, f: (Int) -> HK<F, Boolean> ->
24+
FT.filterA(fa, f, GA).equalUnderTheLaw(fa.traverseFilter(FT, GA) { a -> f(a).map(FT) { b: Boolean -> if (b) Some(a) else None } }, EQ)
25+
})
26+
}

0 commit comments

Comments
 (0)