Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MonadFilter docs and comprehension guards #441

Merged
merged 8 commits into from
Nov 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions kategory-core/src/main/kotlin/kategory/typeclasses/MonadFilter.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package kategory

import kotlin.coroutines.experimental.startCoroutine

interface MonadFilter<F> : Monad<F>, FunctorFilter<F>, Typeclass {

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

/**
* Entry point for monad bindings which enables for comprehension. The underlying impl is based on coroutines.
* A coroutine is initiated and inside [MonadContinuation] suspended yielding to [flatMap]. Once all the flatMap binds are completed
* the underlying monad is returned from the act of executing the coroutine
*/
fun <F, B> MonadFilter<F>.bindingFilter(c: suspend MonadFilterContinuation<F, *>.() -> HK<F, B>): HK<F, B> {
val continuation = MonadFilterContinuation<F, B>(this)
c.startCoroutine(continuation, continuation)
return continuation.returnedMonad()
}

inline fun <reified F> monadFilter(): MonadFilter<F> = instance(InstanceParametrizedType(MonadFilter::class.java, listOf(typeLiteral<F>())))
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kategory

import kotlin.coroutines.experimental.CoroutineContext
import kotlin.coroutines.experimental.EmptyCoroutineContext
import kotlin.coroutines.experimental.RestrictsSuspension

@RestrictsSuspension
open class MonadFilterContinuation<F, A>(val MF: MonadFilter<F>, override val context: CoroutineContext = EmptyCoroutineContext) :
MonadContinuation<F, A>(MF) {

/**
* marker exception that interrupts the coroutine flow and gets captured
* to provide the monad empty value
*/
private object PredicateInterrupted : RuntimeException()

override fun resumeWithException(exception: Throwable) {
when (exception) {
is PredicateInterrupted -> returnedMonad = MF.empty()
else -> super.resumeWithException(exception)
}
}

/**
* Short circuits monadic bind if `predicate == false` return the
* monad `empty` value.
*/
fun continueIf(predicate: Boolean): Unit {
if (!predicate) throw PredicateInterrupted
}

/**
* Binds only if the given predicate matches the inner value otherwise binds into the Monad `empty()` value
* on `MonadFilter` instances
*/
suspend fun <B> HK<F, B>.bindWithFilter(f: (B) -> Boolean): B {
val b: B = bind { this }
return if (f(b)) b else bind { MF.empty<B>() }
}

}
2 changes: 2 additions & 0 deletions kategory-core/src/test/kotlin/kategory/data/ListKWTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kategory

import io.kotlintest.KTestJUnitRunner
import io.kotlintest.matchers.shouldBe
import io.kotlintest.matchers.shouldNotBe
import kategory.laws.EqLaws
import org.junit.runner.RunWith
Expand All @@ -21,6 +22,7 @@ class ListKWTest : UnitSpec() {
semigroup<ListKW<Int>>() shouldNotBe null
monoid<ListKW<Int>>() shouldNotBe null
monoidK<ListKW<ListKWHK>>() shouldNotBe null
monadFilter<ListKWHK>() shouldNotBe null
monadCombine<ListKW<ListKWHK>>() shouldNotBe null
functorFilter<ListKW<ListKWHK>>() shouldNotBe null
monadFilter<ListKW<ListKWHK>>() shouldNotBe null
Expand Down
100 changes: 100 additions & 0 deletions kategory-docs/docs/docs/typeclasses/monadfilter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,103 @@ permalink: /docs/typeclasses/monadfilter/
---

## MonadFilter

`MonadFilter` is a type class that abstracts away the option of interrupting computation if a given predicate is not satisfied.

All instances of `MonadFilter` provide syntax over their respective data types to comprehend monadically over their computation:

##continueWith

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.

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.

When `continueIf` is satisfied the computation continues

```kotlin:ank
import kategory.*

Option.monadFilter().bindingFilter {
val a = Option(1).bind()
val b = Option(1).bind()
val c = a + b
continueIf(c > 0)
yields(c)
}
```

```kotlin:ank
ListKW.monadFilter().bindingFilter {
val a = listOf(1).k().bind()
val b = listOf(1).k().bind()
val c = a + b
continueIf(c > 0)
yields(c)
}
```

When `continueIf` returns `false` the computation is interrupted and the `empty()` value is returned

```kotlin:ank
Option.monadFilter().bindingFilter {
val a = Option(1).bind()
val b = Option(1).bind()
val c = a + b
continueIf(c < 0)
yields(c)
}
```

```kotlin:ank
ListKW.monadFilter().bindingFilter {
val a = listOf(1).k().bind()
val b = listOf(1).k().bind()
val c = a + b
continueIf(c < 0)
yields(c)
}
```

##bindWithFilter

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.

When `bindWithFilter` is satisfied the computation continues

```kotlin:ank
Option.monadFilter().bindingFilter {
val a = Option(1).bind()
val b = Option(1).bindWithFilter { it == a } //continues
val c = a + b
yields(c)
}
```

```kotlin:ank
ListKW.monadFilter().bindingFilter {
val a = listOf(1).k().bind()
val b = listOf(1).k().bindWithFilter { it == a } //continues
val c = a + b
yields(c)
}
```

When `bindWithFilter` returns `false` the computation short circuits yielding the monad's empty value

```kotlin:ank
Option.monadFilter().bindingFilter {
val a = Option(0).bind()
val b = Option(1).bindWithFilter { it == a } //short circuits because a is 0
val c = a + b
yields(c)
}
```

```kotlin:ank
ListKW.monadFilter().bindingFilter {
val a = listOf(0).k().bind()
val b = listOf(1).k().bindWithFilter { it == a } //short circuits because a is 0
val c = a + b
yields(c)
}
```
2 changes: 1 addition & 1 deletion kategory-docs/src/main/java/kategory/debug.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ inline fun <reified F, reified E> debugInstanceLookups(): Map<KClass<out Typecla
)

inline fun <reified F, reified E> showInstances(
debugLookupTable: Map<KClass<out Typeclass>, () -> Typeclass> = debugInstanceLookups<F, E>()) =
debugLookupTable: Map<KClass<out Typeclass>, () -> Typeclass> = debugInstanceLookups<F, E>()): List<String?> =
debugLookupTable.entries
.filter { Try { it.value() }.fold({ false }, { true }) }
.map { it.key.simpleName }
25 changes: 22 additions & 3 deletions kategory-test/src/main/kotlin/kategory/laws/MonadFilterLaws.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ object MonadFilterLaws {
inline fun <reified F> laws(MF: MonadFilter<F> = monadFilter<F>(), crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<HK<F, Int>>): List<Law> =
MonadLaws.laws(MF, EQ) + FunctorFilterLaws.laws(MF, cf, EQ) + listOf(
Law("MonadFilter Laws: Left Empty", { monadFilterLeftEmpty(MF, EQ) }),
Law("MonadFilter Laws: Right Empty", { monadFilterRightEmpty(MF, cf, EQ) }),
Law("MonadFilter Laws: Consistency", { monadFilterConsistency(MF, cf, EQ) }))
Law("MonadFilter Laws: Right Empty", { monadFilterRightEmpty(MF, EQ) }),
Law("MonadFilter Laws: Consistency", { monadFilterConsistency(MF, cf, EQ) }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use applicative pure instead of constructor function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just removed that cf param because it was not used but did not added anything to that in this PR. But presumably yes.

Law("MonadFilter Laws: Comprehension Guards", { monadFilterEmptyComprehensions(MF, EQ) }),
Law("MonadFilter Laws: Comprehension bindWithFilter Guards", { monadFilterBindWithFilterComprehensions(MF, EQ) }))

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

inline fun <reified F> monadFilterRightEmpty(MF: MonadFilter<F> = monadFilter<F>(), crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<HK<F, Int>>): Unit =
inline fun <reified F> monadFilterRightEmpty(MF: MonadFilter<F> = monadFilter<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genApplicative(Gen.int(), MF), { fa: HK<F, Int> ->
MF.flatMap(fa, { MF.empty<Int>() }).equalUnderTheLaw(MF.empty(), EQ)
})
Expand All @@ -25,4 +27,21 @@ object MonadFilterLaws {
forAll(genFunctionAToB(Gen.bool()), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
MF.filter(fa, f).equalUnderTheLaw(fa.flatMap(MF, { a -> if (f(a)) MF.pure(a) else MF.empty() }), EQ)
})

inline fun <reified F> monadFilterEmptyComprehensions(MF: MonadFilter<F> = monadFilter<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(Gen.bool(), Gen.int(), { guard: Boolean, n: Int ->
MF.bindingFilter {
continueIf(guard)
yields(n)
}.equalUnderTheLaw(if (!guard) MF.empty() else MF.pure(n), EQ)
})

inline fun <reified F> monadFilterBindWithFilterComprehensions(MF: MonadFilter<F> = monadFilter<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(Gen.bool(), Gen.int(), { guard: Boolean, n: Int ->
MF.bindingFilter {
val x = MF.pure(n).bindWithFilter { _ -> guard }
yields(x)
}.equalUnderTheLaw(if (!guard) MF.empty() else MF.pure(n), EQ)
})

}