Skip to content

Commit 24b9600

Browse files
authored
Merge pull request #441 from kategory/rr-monad-filter-comprehensions-guards
MonadFilter docs and comprehension guards
2 parents 8b9a098 + 4358f6e commit 24b9600

File tree

6 files changed

+179
-4
lines changed

6 files changed

+179
-4
lines changed

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

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package kategory
22

3+
import kotlin.coroutines.experimental.startCoroutine
4+
35
interface MonadFilter<F> : Monad<F>, FunctorFilter<F>, Typeclass {
46

57
fun <A> empty(): HK<F, A>
@@ -8,4 +10,15 @@ interface MonadFilter<F> : Monad<F>, FunctorFilter<F>, Typeclass {
810
flatMap(fa, { a -> f(a).fold({ empty<B>() }, { pure(it) }) })
911
}
1012

13+
/**
14+
* Entry point for monad bindings which enables for comprehension. The underlying impl is based on coroutines.
15+
* A coroutine is initiated and inside [MonadContinuation] suspended yielding to [flatMap]. Once all the flatMap binds are completed
16+
* the underlying monad is returned from the act of executing the coroutine
17+
*/
18+
fun <F, B> MonadFilter<F>.bindingFilter(c: suspend MonadFilterContinuation<F, *>.() -> HK<F, B>): HK<F, B> {
19+
val continuation = MonadFilterContinuation<F, B>(this)
20+
c.startCoroutine(continuation, continuation)
21+
return continuation.returnedMonad()
22+
}
23+
1124
inline fun <reified F> monadFilter(): MonadFilter<F> = instance(InstanceParametrizedType(MonadFilter::class.java, listOf(typeLiteral<F>())))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package kategory
2+
3+
import kotlin.coroutines.experimental.CoroutineContext
4+
import kotlin.coroutines.experimental.EmptyCoroutineContext
5+
import kotlin.coroutines.experimental.RestrictsSuspension
6+
7+
@RestrictsSuspension
8+
open class MonadFilterContinuation<F, A>(val MF: MonadFilter<F>, override val context: CoroutineContext = EmptyCoroutineContext) :
9+
MonadContinuation<F, A>(MF) {
10+
11+
/**
12+
* marker exception that interrupts the coroutine flow and gets captured
13+
* to provide the monad empty value
14+
*/
15+
private object PredicateInterrupted : RuntimeException()
16+
17+
override fun resumeWithException(exception: Throwable) {
18+
when (exception) {
19+
is PredicateInterrupted -> returnedMonad = MF.empty()
20+
else -> super.resumeWithException(exception)
21+
}
22+
}
23+
24+
/**
25+
* Short circuits monadic bind if `predicate == false` return the
26+
* monad `empty` value.
27+
*/
28+
fun continueIf(predicate: Boolean): Unit {
29+
if (!predicate) throw PredicateInterrupted
30+
}
31+
32+
/**
33+
* Binds only if the given predicate matches the inner value otherwise binds into the Monad `empty()` value
34+
* on `MonadFilter` instances
35+
*/
36+
suspend fun <B> HK<F, B>.bindWithFilter(f: (B) -> Boolean): B {
37+
val b: B = bind { this }
38+
return if (f(b)) b else bind { MF.empty<B>() }
39+
}
40+
41+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kategory
22

33
import io.kotlintest.KTestJUnitRunner
4+
import io.kotlintest.matchers.shouldBe
45
import io.kotlintest.matchers.shouldNotBe
56
import kategory.laws.EqLaws
67
import org.junit.runner.RunWith
@@ -21,6 +22,7 @@ class ListKWTest : UnitSpec() {
2122
semigroup<ListKW<Int>>() shouldNotBe null
2223
monoid<ListKW<Int>>() shouldNotBe null
2324
monoidK<ListKW<ListKWHK>>() shouldNotBe null
25+
monadFilter<ListKWHK>() shouldNotBe null
2426
monadCombine<ListKW<ListKWHK>>() shouldNotBe null
2527
functorFilter<ListKW<ListKWHK>>() shouldNotBe null
2628
monadFilter<ListKW<ListKWHK>>() shouldNotBe null

kategory-docs/docs/docs/typeclasses/monadfilter/README.md

+100
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,103 @@ permalink: /docs/typeclasses/monadfilter/
55
---
66

77
## MonadFilter
8+
9+
`MonadFilter` is a type class that abstracts away the option of interrupting computation if a given predicate is not satisfied.
10+
11+
All instances of `MonadFilter` provide syntax over their respective data types to comprehend monadically over their computation:
12+
13+
##continueWith
14+
15+
Binding over `MonadFilter` instances with `bindingFilter` brings into scope the `continueIf` guard that requires a `Boolean` predicate as value. If the predicate is `true` the computation will continue and if the predicate returns `false` the computation is short-circuited returning monad filter instance `empty()` value.
16+
17+
In the example below we demonstrate monadic comprehension over the `MonadFilter` instances for both `Option` and `ListKW` since both data types can provide a safe `empty` value.
18+
19+
When `continueIf` is satisfied the computation continues
20+
21+
```kotlin:ank
22+
import kategory.*
23+
24+
Option.monadFilter().bindingFilter {
25+
val a = Option(1).bind()
26+
val b = Option(1).bind()
27+
val c = a + b
28+
continueIf(c > 0)
29+
yields(c)
30+
}
31+
```
32+
33+
```kotlin:ank
34+
ListKW.monadFilter().bindingFilter {
35+
val a = listOf(1).k().bind()
36+
val b = listOf(1).k().bind()
37+
val c = a + b
38+
continueIf(c > 0)
39+
yields(c)
40+
}
41+
```
42+
43+
When `continueIf` returns `false` the computation is interrupted and the `empty()` value is returned
44+
45+
```kotlin:ank
46+
Option.monadFilter().bindingFilter {
47+
val a = Option(1).bind()
48+
val b = Option(1).bind()
49+
val c = a + b
50+
continueIf(c < 0)
51+
yields(c)
52+
}
53+
```
54+
55+
```kotlin:ank
56+
ListKW.monadFilter().bindingFilter {
57+
val a = listOf(1).k().bind()
58+
val b = listOf(1).k().bind()
59+
val c = a + b
60+
continueIf(c < 0)
61+
yields(c)
62+
}
63+
```
64+
65+
##bindWithFilter
66+
67+
Binding over `MonadFilter` instances with `bindingFilter` brings into scope the `bindWithFilter` guard that requires a `Boolean` predicate as value getting matched on the monad capturing inner value. If the predicate is `true` the computation will continue and if the predicate returns `false` the computation is short-circuited returning the monad filter instance `empty()` value.
68+
69+
When `bindWithFilter` is satisfied the computation continues
70+
71+
```kotlin:ank
72+
Option.monadFilter().bindingFilter {
73+
val a = Option(1).bind()
74+
val b = Option(1).bindWithFilter { it == a } //continues
75+
val c = a + b
76+
yields(c)
77+
}
78+
```
79+
80+
```kotlin:ank
81+
ListKW.monadFilter().bindingFilter {
82+
val a = listOf(1).k().bind()
83+
val b = listOf(1).k().bindWithFilter { it == a } //continues
84+
val c = a + b
85+
yields(c)
86+
}
87+
```
88+
89+
When `bindWithFilter` returns `false` the computation short circuits yielding the monad's empty value
90+
91+
```kotlin:ank
92+
Option.monadFilter().bindingFilter {
93+
val a = Option(0).bind()
94+
val b = Option(1).bindWithFilter { it == a } //short circuits because a is 0
95+
val c = a + b
96+
yields(c)
97+
}
98+
```
99+
100+
```kotlin:ank
101+
ListKW.monadFilter().bindingFilter {
102+
val a = listOf(0).k().bind()
103+
val b = listOf(1).k().bindWithFilter { it == a } //short circuits because a is 0
104+
val c = a + b
105+
yields(c)
106+
}
107+
```

kategory-docs/src/main/java/kategory/debug.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ inline fun <reified F, reified E> debugInstanceLookups(): Map<KClass<out Typecla
3333
)
3434

3535
inline fun <reified F, reified E> showInstances(
36-
debugLookupTable: Map<KClass<out Typeclass>, () -> Typeclass> = debugInstanceLookups<F, E>()) =
36+
debugLookupTable: Map<KClass<out Typeclass>, () -> Typeclass> = debugInstanceLookups<F, E>()): List<String?> =
3737
debugLookupTable.entries
3838
.filter { Try { it.value() }.fold({ false }, { true }) }
3939
.map { it.key.simpleName }

kategory-test/src/main/kotlin/kategory/laws/MonadFilterLaws.kt

+22-3
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ object MonadFilterLaws {
88
inline fun <reified F> laws(MF: MonadFilter<F> = monadFilter<F>(), crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<HK<F, Int>>): List<Law> =
99
MonadLaws.laws(MF, EQ) + FunctorFilterLaws.laws(MF, cf, EQ) + listOf(
1010
Law("MonadFilter Laws: Left Empty", { monadFilterLeftEmpty(MF, EQ) }),
11-
Law("MonadFilter Laws: Right Empty", { monadFilterRightEmpty(MF, cf, EQ) }),
12-
Law("MonadFilter Laws: Consistency", { monadFilterConsistency(MF, cf, EQ) }))
11+
Law("MonadFilter Laws: Right Empty", { monadFilterRightEmpty(MF, EQ) }),
12+
Law("MonadFilter Laws: Consistency", { monadFilterConsistency(MF, cf, EQ) }),
13+
Law("MonadFilter Laws: Comprehension Guards", { monadFilterEmptyComprehensions(MF, EQ) }),
14+
Law("MonadFilter Laws: Comprehension bindWithFilter Guards", { monadFilterBindWithFilterComprehensions(MF, EQ) }))
1315

1416
inline fun <reified F> monadFilterLeftEmpty(MF: MonadFilter<F> = monadFilter<F>(), EQ: Eq<HK<F, Int>>): Unit =
1517
forAll(genFunctionAToB(genApplicative(Gen.int(), MF)), { f: (Int) -> HK<F, Int> ->
1618
MF.empty<Int>().flatMap(MF, f).equalUnderTheLaw(MF.empty(), EQ)
1719
})
1820

19-
inline fun <reified F> monadFilterRightEmpty(MF: MonadFilter<F> = monadFilter<F>(), crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<HK<F, Int>>): Unit =
21+
inline fun <reified F> monadFilterRightEmpty(MF: MonadFilter<F> = monadFilter<F>(), EQ: Eq<HK<F, Int>>): Unit =
2022
forAll(genApplicative(Gen.int(), MF), { fa: HK<F, Int> ->
2123
MF.flatMap(fa, { MF.empty<Int>() }).equalUnderTheLaw(MF.empty(), EQ)
2224
})
@@ -25,4 +27,21 @@ object MonadFilterLaws {
2527
forAll(genFunctionAToB(Gen.bool()), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
2628
MF.filter(fa, f).equalUnderTheLaw(fa.flatMap(MF, { a -> if (f(a)) MF.pure(a) else MF.empty() }), EQ)
2729
})
30+
31+
inline fun <reified F> monadFilterEmptyComprehensions(MF: MonadFilter<F> = monadFilter<F>(), EQ: Eq<HK<F, Int>>): Unit =
32+
forAll(Gen.bool(), Gen.int(), { guard: Boolean, n: Int ->
33+
MF.bindingFilter {
34+
continueIf(guard)
35+
yields(n)
36+
}.equalUnderTheLaw(if (!guard) MF.empty() else MF.pure(n), EQ)
37+
})
38+
39+
inline fun <reified F> monadFilterBindWithFilterComprehensions(MF: MonadFilter<F> = monadFilter<F>(), EQ: Eq<HK<F, Int>>): Unit =
40+
forAll(Gen.bool(), Gen.int(), { guard: Boolean, n: Int ->
41+
MF.bindingFilter {
42+
val x = MF.pure(n).bindWithFilter { _ -> guard }
43+
yields(x)
44+
}.equalUnderTheLaw(if (!guard) MF.empty() else MF.pure(n), EQ)
45+
})
46+
2847
}

0 commit comments

Comments
 (0)