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

Add fixes to Monads found by new Law checks #120

Merged
merged 14 commits into from
Jun 27, 2017
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Fri Jun 09 20:51:23 CEST 2017
#Sun Jun 25 18:21:54 BST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
Expand Down
10 changes: 5 additions & 5 deletions kategory/src/main/kotlin/kategory/instances/Function0Bimonad.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ data class Function0<out A>(internal val f: () -> A) : HK<Function0.F, A> {
pure(f(fa.ev().invoke()))

override fun <A, B> tailRecM(a: A, f: (A) -> HK<F, Either<A, B>>): HK<F, B> =
f(a).ev().invoke().let { either ->
when (either) {
is Either.Left -> tailRecM(either.a, f)
is Either.Right -> ({ either.b }).k()
}
Function0 {
tailrec fun loop(thisA: A): B =
f(thisA).ev().invoke().fold({ loop(it) }, { it })

loop(a)
}
}
}
3 changes: 3 additions & 0 deletions kategory/src/main/kotlin/kategory/instances/IdMonad.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ interface IdMonad : Monad<Id.F> {
override fun <A, B> flatMap(fa: IdKind<A>, f: (A) -> IdKind<B>): Id<B> =
fa.ev().flatMap { f(it).ev() }

override fun <A, B> map(fa: HK<Id.F, A>, f: (A) -> B): Id<B> =
fa.ev().map(f)

@Suppress("UNCHECKED_CAST")
tailrec override fun <A, B> tailRecM(a: A, f: (A) -> IdKind<Either<A, B>>): Id<B> {
val x = f(a).ev().value
Expand Down
1 change: 0 additions & 1 deletion kategory/src/main/kotlin/kategory/instances/OptionMonad.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package kategory

interface OptionMonad : Monad<Option.F> {

override fun <A, B> map(fa: OptionKind<A>, f: (A) -> B): Option<B> =
fa.ev().map(f)

Expand Down
8 changes: 2 additions & 6 deletions kategory/src/main/kotlin/kategory/instances/TryMonadError.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ interface TryMonadError : MonadError<Try.F, Throwable> {
fa.ev().recoverWith { f(it).ev() }

@Suppress("UNCHECKED_CAST")
override fun <A, B> tailRecM(a: A, f: (A) -> TryKind<Either<A, B>>): Try<B> {
val x = f(a).ev()
return if (x is Try.Success && x.value is Either.Left<A>) tailRecM(x.value.a, f)
else if (x is Try.Success && x.value is Either.Right<B>) Try.Success(x.value.b)
else x as Try.Failure<B>
}
override fun <A, B> tailRecM(a: A, f: (A) -> TryKind<Either<A, B>>): Try<B> =
f(a).ev().fold({ Try.raiseError(it) }, { either -> either.fold({ tailRecM(it, f) }, { Try.Success(it) }) })
}
18 changes: 18 additions & 0 deletions kategory/src/main/kotlin/kategory/typeclasses/Eq.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kategory

interface Eq<in F> : Typeclass {
Copy link
Member

Choose a reason for hiding this comment

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

👏

fun eqv(a: F, b: F): Boolean

fun neqv(a: F, b: F): Boolean =
!eqv(a, b)

companion object {
operator fun <F> invoke() = object : Eq<F> {
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be a default instance of Eq[Any] outside of this object.

override fun eqv(a: F, b: F): Boolean =
a == b

override fun neqv(a: F, b: F): Boolean =
a != b
}
}
}
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/EitherTTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.junit.runner.RunWith
class EitherTTest : UnitSpec() {
init {

testLaws(MonadLaws.laws(EitherTMonad<Id.F, Int>()))
testLaws(MonadLaws.laws(EitherTMonad<Id.F, Int>(), Eq()))
Copy link
Member

Choose a reason for hiding this comment

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

Probably better named as EqAny


"map should modify value" {
forAll { a: String ->
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/EitherTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.junit.runner.RunWith
class EitherTest : UnitSpec() {
init {

testLaws(MonadLaws.laws(EitherMonad<Int>()))
testLaws(MonadLaws.laws(EitherMonad<Int>(), Eq()))

"map should modify value" {
forAll { a: Int, b: String ->
Expand Down
5 changes: 4 additions & 1 deletion kategory/src/test/kotlin/kategory/data/EvalTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import org.junit.runner.RunWith
class EvalTest : UnitSpec() {
init {

testLaws(MonadLaws.laws(Eval))
testLaws(MonadLaws.laws(Eval, object : Eq<HK<Eval.F, Int>> {
Copy link
Member

Choose a reason for hiding this comment

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

We should have this instance in the core not just in tests and there should be an instance for each one of the datatypes we implement.

override fun eqv(a: HK<Eval.F, Int>, b: HK<Eval.F, Int>): Boolean =
a.ev().value() == b.ev().value()
}))

"should map wrapped value" {
val sideEffect = SideEffect()
Expand Down
5 changes: 4 additions & 1 deletion kategory/src/test/kotlin/kategory/data/Function0Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import org.junit.runner.RunWith
class Function0Test : UnitSpec() {
init {

testLaws(MonadLaws.laws(Function0))
testLaws(MonadLaws.laws(Function0, object : Eq<HK<Function0.F, Int>> {
Copy link
Member

Choose a reason for hiding this comment

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

We should have this instance in the core not just in tests.

override fun eqv(a: HK<Function0.F, Int>, b: HK<Function0.F, Int>): Boolean =
a.ev()() == b.ev()()
}))

"Function0Monad.binding should for comprehend over all values of multiple Function0" {
Function0.binding {
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/IdTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.junit.runner.RunWith
class IdTest : UnitSpec() {
init {

testLaws(MonadLaws.laws(Id))
testLaws(MonadLaws.laws(Id, Eq()))

"IdMonad.binding should for comprehend over all values of multiple Ids" {
Id.binding {
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/IorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class IorTest : UnitSpec() {

val intIorMonad = IorMonad(IntMonoid)

testLaws(MonadLaws.laws(intIorMonad))
testLaws(MonadLaws.laws(intIorMonad, Eq()))

"flatMap() should modify entity" {
forAll { a: Int, b: String ->
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/NonEmptyListTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.junit.runner.RunWith
class NonEmptyListTest : UnitSpec() {
init {

testLaws(MonadLaws.laws(NonEmptyList))
testLaws(MonadLaws.laws(NonEmptyList, Eq()))

"map should modify values" {
NonEmptyList.of(14).map { it * 3 } shouldBe NonEmptyList.of(42)
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/OptionTTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.junit.runner.RunWith
class OptionTTest : UnitSpec() {
init {

testLaws(MonadLaws.laws(OptionTMonad(NonEmptyList)))
testLaws(MonadLaws.laws(OptionTMonad(NonEmptyList), Eq()))

"map should modify value" {
forAll { a: String ->
Expand Down
4 changes: 2 additions & 2 deletions kategory/src/test/kotlin/kategory/data/OptionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import io.kotlintest.KTestJUnitRunner
import io.kotlintest.matchers.fail
import io.kotlintest.matchers.shouldBe
import io.kotlintest.properties.forAll
import kategory.Option.Some
import kategory.Option.None
import kategory.Option.Some
import org.junit.runner.RunWith

@RunWith(KTestJUnitRunner::class)
class OptionTest: UnitSpec() {

init {

testLaws(MonadLaws.laws(Option))
testLaws(MonadLaws.laws(Option, Eq()))

"map should modify value" {
Some(12).map { "flower" } shouldBe Some("flower")
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/TryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class TryTest : UnitSpec() {

init {

testLaws(MonadLaws.laws(Try))
testLaws(MonadLaws.laws(Try, Eq()))

"invoke of any should be success" {
Try.invoke { 1 } shouldBe Success(1)
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/ValidatedTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ValidatedTest : UnitSpec() {
override fun combine(a: String, b: String): String = "$a $b"
}

testLaws(ApplicativeLaws.laws(ValidatedApplicativeError(concatStringSG)))
testLaws(ApplicativeLaws.laws(ValidatedApplicativeError(concatStringSG), Eq()))

"fold should call function on Invalid" {
val exception = Exception("My Exception")
Expand Down
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/data/WriterTTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.junit.runner.RunWith
class WriterTTest : UnitSpec() {
init {

testLaws(MonadLaws.laws(WriterTMonad(NonEmptyList, IntMonoid)))
testLaws(MonadLaws.laws(WriterTMonad(NonEmptyList, IntMonoid), Eq()))

"tell should accumulate write" {
forAll { a: Int ->
Expand Down
9 changes: 6 additions & 3 deletions kategory/src/test/kotlin/kategory/free/FreeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class FreeTest : UnitSpec() {

val program = Ops.binding {
val added = !Ops.add(10, 10)
val substracted = !Ops.subtract(added, 50)
yields(substracted)
val subtracted = !Ops.subtract(added, 50)
yields(subtracted)
}.ev()

fun stackSafeTestProgram(n: Int, stopAt: Int): Free<Ops.F, Int> = Ops.binding {
Expand All @@ -38,7 +38,10 @@ class FreeTest : UnitSpec() {

init {

testLaws(MonadLaws.laws(Ops))
testLaws(MonadLaws.laws(Ops, object : Eq<HK<FreeF<Ops.F>, Int>> {
override fun eqv(a: HK<FreeF<Ops.F>, Int>, b: HK<FreeF<Ops.F>, Int>): Boolean =
a.ev().foldMap(idInterpreter, Id) == b.ev().foldMap(idInterpreter, Id)
}))

"Can interpret an ADT as Free operations" {
program.foldMap(optionInterpreter, Option).ev() shouldBe Option.Some(-30)
Expand Down
28 changes: 14 additions & 14 deletions kategory/src/test/kotlin/kategory/laws/ApplicativeLaws.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,32 @@ import io.kotlintest.properties.forAll

object ApplicativeLaws {

inline fun <reified F> laws(A: Applicative<F> = applicative<F>()): List<Law> =
FunctorLaws.laws(A) + listOf(
Law("Applicative Laws: ap identity", { apIdentity(A) }),
Law("Applicative Laws: homomorphism", { homomorphism(A) }),
Law("Applicative Laws: interchange", { interchange(A) }),
Law("Applicative Laws: map derived", { mapDerived(A) })
inline fun <reified F> laws(A: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>>): List<Law> =
FunctorLaws.laws(A, EQ) + listOf(
Law("Applicative Laws: ap identity", { apIdentity(A, EQ) }),
Law("Applicative Laws: homomorphism", { homomorphism(A, EQ) }),
Law("Applicative Laws: interchange", { interchange(A, EQ) }),
Law("Applicative Laws: map derived", { mapDerived(A, EQ) })
)

inline fun <reified F> apIdentity(A: Applicative<F> = applicative<F>()): Unit =
inline fun <reified F> apIdentity(A: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genApplicative(Gen.int(), A), { fa: HK<F, Int> ->
A.ap(fa, A.pure({ n: Int -> n })) == fa
A.ap(fa, A.pure({ n: Int -> n })).equalUnderTheLaw(fa, EQ)
})

inline fun <reified F> homomorphism(A: Applicative<F> = applicative<F>()): Unit =
inline fun <reified F> homomorphism(A: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genFunctionAToB<Int, Int>(Gen.int()), Gen.int(), { ab: (Int) -> Int, a: Int ->
A.ap(A.pure(a), A.pure(ab)) == A.pure(ab(a))
A.ap(A.pure(a), A.pure(ab)).equalUnderTheLaw(A.pure(ab(a)), EQ)
})

inline fun <reified F> interchange(A: Applicative<F> = applicative<F>()): Unit =
inline fun <reified F> interchange(A: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genApplicative(genFunctionAToB<Int, Int>(Gen.int()), A), Gen.int(), { fa: HK<F, (Int) -> Int>, a: Int ->
A.ap(A.pure(a), fa) == A.ap(fa, A.pure({ x: (Int) -> Int -> x(a) }))
A.ap(A.pure(a), fa).equalUnderTheLaw(A.ap(fa, A.pure({ x: (Int) -> Int -> x(a) })), EQ)
})

inline fun <reified F> mapDerived(A: Applicative<F> = applicative<F>()): Unit =
inline fun <reified F> mapDerived(A: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genApplicative(Gen.int(), A), genFunctionAToB<Int, Int>(Gen.int()), { fa: HK<F, Int>, f: (Int) -> Int ->
A.map(fa, f) == A.ap(fa, A.pure(f))
A.map(fa, f).equalUnderTheLaw(A.ap(fa, A.pure(f)), EQ)
})

}
14 changes: 7 additions & 7 deletions kategory/src/test/kotlin/kategory/laws/FunctorLaws.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ import io.kotlintest.properties.forAll

object FunctorLaws {

inline fun <reified F> laws(AP: Applicative<F> = applicative<F>()): List<Law> =
inline fun <reified F> laws(AP: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>>): List<Law> =
listOf(
Law("Functor Laws: Covariant Identity", { covariantIdentity(AP) }),
Law("Functor: Covariant Composition", { covariantComposition(AP) })
Law("Functor Laws: Covariant Identity", { covariantIdentity(AP, EQ) }),
Law("Functor: Covariant Composition", { covariantComposition(AP, EQ) })
)

inline fun <reified F> covariantIdentity(AP: Applicative<F> = applicative<F>()): Unit =
inline fun <reified F> covariantIdentity(AP: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>> = Eq()): Unit =
forAll(genApplicative(Gen.int(), AP), { fa: HK<F, Int> ->
AP.map(fa, ::identity) == fa
AP.map(fa, ::identity).equalUnderTheLaw(fa, EQ)
})

inline fun <reified F> covariantComposition(AP: Applicative<F> = applicative<F>()): Unit =
inline fun <reified F> covariantComposition(AP: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>> = Eq()): Unit =
forAll(
genApplicative(Gen.int(), AP),
genFunctionAToB<Int, Int>(Gen.int()),
genFunctionAToB<Int, Int>(Gen.int()),
{ fa: HK<F, Int>, f, g ->
AP.map(AP.map(fa, f), g) == AP.map(fa, f andThen g)
AP.map(AP.map(fa, f), g).equalUnderTheLaw(AP.map(fa, f andThen g), EQ)
}
)

Expand Down
2 changes: 2 additions & 0 deletions kategory/src/test/kotlin/kategory/laws/Law.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ package kategory

data class Law(val name: String, val test: () -> Unit)

inline fun <reified A> A.equalUnderTheLaw(b: A, eq: Eq<A> = Eq()): Boolean =
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 have extension functions that are inline reify... <F, ..> Once we have instances for the datatypes as part of our effort to provide syntax all datatypes will include syntax for eqv and neqv which can be used directly here provided thre is a registeres Eq instance for the datatype being compared.

eq.eqv(this, b)
44 changes: 21 additions & 23 deletions kategory/src/test/kotlin/kategory/laws/MonadLaws.kt
Original file line number Diff line number Diff line change
@@ -1,49 +1,47 @@
package kategory

import io.kotlintest.matchers.shouldBe
import io.kotlintest.properties.Gen
import io.kotlintest.properties.forAll

object MonadLaws {

inline fun <reified F> laws(M: Monad<F> = monad<F>()): List<Law> =
ApplicativeLaws.laws(M) + listOf(
Law("Monad Laws: left identity", { leftIdentity(M) }),
Law("Monad Laws: right identity", { rightIdentity(M) }),
Law("Monad Laws: kleisli left identity", { kleisliLeftIdentity(M) }),
Law("Monad Laws: kleisli right identity", { kleisliRightIdentity(M) }),
Law("Monad Laws: map / flatMap coherence", { mapFlatMapCoherence(M) }),
Law("Monad / JVM: stack safe", { mapFlatMapCoherence(M) })
Copy link
Member

Choose a reason for hiding this comment

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

Thanks!

inline fun <reified F> laws(M: Monad<F> = monad<F>(), EQ: Eq<HK<F, Int>>): List<Law> =
ApplicativeLaws.laws(M, EQ) + listOf(
Law("Monad Laws: left identity", { leftIdentity(M, EQ) }),
Law("Monad Laws: right identity", { rightIdentity(M, EQ) }),
Law("Monad Laws: kleisli left identity", { kleisliLeftIdentity(M, EQ) }),
Law("Monad Laws: kleisli right identity", { kleisliRightIdentity(M, EQ) }),
Law("Monad Laws: map / flatMap coherence", { mapFlatMapCoherence(M, EQ) }),
Law("Monad / JVM: stack safe", { stackSafety(5000, M) })
)

inline fun <reified F> leftIdentity(M: Monad<F> = monad<F>()): Unit =
inline fun <reified F> leftIdentity(M: Monad<F> = monad<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genFunctionAToB<Int, HK<F, Int>>(genApplicative(Gen.int(), M)), Gen.int(), { f: (Int) -> HK<F, Int>, a: Int ->
M.flatMap(M.pure(a), f) == f(a)
M.flatMap(M.pure(a), f).equalUnderTheLaw(f(a), EQ)
})

inline fun <reified F> rightIdentity(M: Monad<F> = monad<F>()): Unit =
inline fun <reified F> rightIdentity(M: Monad<F> = monad<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genApplicative(Gen.int(), M), { fa: HK<F, Int> ->
M.flatMap(fa, { M.pure(it) }) == fa
M.flatMap(fa, { M.pure(it) }).equalUnderTheLaw(fa, EQ)
})

inline fun <reified F> kleisliLeftIdentity(M: Monad<F> = monad<F>()): Unit =
inline fun <reified F> kleisliLeftIdentity(M: Monad<F> = monad<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genFunctionAToB<Int, HK<F, Int>>(genApplicative(Gen.int(), M)), Gen.int(), { f: (Int) -> HK<F, Int>, a: Int ->
(Kleisli({ n : Int -> M.pure(n)}, M) andThen Kleisli(f, M)).run(a) == f(a)
(Kleisli({ n: Int -> M.pure(n) }, M) andThen Kleisli(f, M)).run(a).equalUnderTheLaw(f(a), EQ)
})

inline fun <reified F> kleisliRightIdentity(M: Monad<F> = monad<F>()): Unit =
inline fun <reified F> kleisliRightIdentity(M: Monad<F> = monad<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genFunctionAToB<Int, HK<F, Int>>(genApplicative(Gen.int(), M)), Gen.int(), { f: (Int) -> HK<F, Int>, a: Int ->
(Kleisli(f, M) andThen Kleisli({ n : Int -> M.pure(n)}, M)).run(a) == f(a)
(Kleisli(f, M) andThen Kleisli({ n: Int -> M.pure(n) }, M)).run(a).equalUnderTheLaw(f(a), EQ)
})

inline fun <reified F> mapFlatMapCoherence(M: Monad<F> = monad<F>()): Unit =
inline fun <reified F> mapFlatMapCoherence(M: Monad<F> = monad<F>(), EQ: Eq<HK<F, Int>>): Unit =
forAll(genFunctionAToB<Int, Int>(Gen.int()), genApplicative(Gen.int(), M), { f: (Int) -> Int, fa: HK<F, Int> ->
M.flatMap(fa, { M.pure(f(it))}) == M.map(fa, f)
M.flatMap(fa, { M.pure(f(it)) }).equalUnderTheLaw(M.map(fa, f), EQ)
})

inline fun <reified F> stackSafety(iterations : Int = 5000, M: Monad<F> = monad<F>()): Unit {
val res = M.tailRecM(0, { i -> M.pure(if (i < iterations) Either.Left(i + 1) else Either.Right(i))})
res shouldBe iterations
inline fun <reified F> stackSafety(iterations: Int = 5000, M: Monad<F> = monad<F>()): Unit {
val res = M.tailRecM(0, { i -> M.pure(if (i < iterations) Either.Left(i + 1) else Either.Right(i)) })
res.equalUnderTheLaw(iterations)
}

}