Skip to content

Commit 3160501

Browse files
authored
Merge pull request #236 from kategory/paco-aperrorlaws
Add ApplicativeError laws
2 parents b072521 + 0a4a969 commit 3160501

File tree

12 files changed

+122
-29
lines changed

12 files changed

+122
-29
lines changed

kategory-effects-test/src/main/kotlin/kategory/laws/AsyncLaws.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import io.kotlintest.properties.Gen
44
import io.kotlintest.properties.forAll
55

66
object AsyncLaws {
7-
inline fun <reified F> laws(AC: AsyncContext<F> = asyncContext(), M: MonadError<F, Throwable> = monadError<F, Throwable>(), EQ: Eq<HK<F, Int>>, EQERR: Eq<HK<F, Int>> = EQ): List<Law> =
8-
MonadErrorLaws.laws(M, EQERR, EQ) + listOf(
7+
inline fun <reified F> laws(AC: AsyncContext<F> = asyncContext(), M: MonadError<F, Throwable> = monadError<F, Throwable>(), EQ: Eq<HK<F, Int>>, EQ_EITHER: Eq<HK<F, Either<Throwable, Int>>>, EQERR: Eq<HK<F, Int>> = EQ): List<Law> =
8+
MonadErrorLaws.laws(M, EQERR, EQ_EITHER, EQ) + listOf(
99
Law("Async Laws: success equivalence", { asyncSuccess(AC, M, EQ) }),
1010
Law("Async Laws: error equivalence", { asyncError(AC, M, EQERR) }),
1111
Law("Async bind: binding blocks", { asyncBind(AC, M, EQ) }),

kategory-effects/src/test/kotlin/kategory/effects/data/IOTest.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import org.junit.runner.RunWith
88

99
@RunWith(KTestJUnitRunner::class)
1010
class IOTest : UnitSpec() {
11-
val EQ: Eq<HK<IOHK, Int>> = object : Eq<HK<IOHK, Int>> {
12-
override fun eqv(a: HK<IOHK, Int>, b: HK<IOHK, Int>): Boolean =
11+
fun <A> EQ(): Eq<HK<IOHK, A>> = object : Eq<HK<IOHK, A>> {
12+
override fun eqv(a: HK<IOHK, A>, b: HK<IOHK, A>): Boolean =
1313
a.ev().attempt().unsafeRunSync() == b.ev().attempt().unsafeRunSync()
1414
}
1515

1616
init {
17-
testLaws(AsyncLaws.laws(IO.asyncContext(), IO.monadError(), EQ, EQ))
17+
testLaws(AsyncLaws.laws(IO.asyncContext(), IO.monadError(), EQ(), EQ()))
1818

1919
"should defer evaluation until run" {
2020
var run = false

kategory-effects/src/test/kotlin/kategory/effects/data/JobKWTest.kt

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ import java.util.concurrent.atomic.AtomicReference
99
import kotlin.coroutines.experimental.EmptyCoroutineContext
1010

1111
@RunWith(KTestJUnitRunner::class)
12-
class JobKWTest : UnitSpec() {
13-
val EQ: Eq<HK<JobKWHK, Int>> = object : Eq<HK<JobKWHK, Int>> {
14-
override fun eqv(a: HK<JobKWHK, Int>, b: HK<JobKWHK, Int>): Boolean =
12+
class JobWTest : UnitSpec() {
13+
private fun <A> EQ(): Eq<HK<JobKWHK, A>> = object : Eq<HK<JobKWHK, A>> {
14+
override fun eqv(a: HK<JobKWHK, A>, b: HK<JobKWHK, A>): Boolean =
1515
runBlocking {
16-
val resultA = AtomicInteger(Int.MIN_VALUE)
17-
val resultB = AtomicInteger(Int.MAX_VALUE)
16+
val resultA = AtomicReference<A>()
17+
val resultB = AtomicReference<A>()
1818
val success = AtomicBoolean(true)
1919
a.runJob {
2020
it.fold({ success.set(false) }, { resultA.set(it) })
2121
}.join()
2222
b.runJob({
2323
it.fold({ success.set(false) }, { resultB.set(it) })
2424
}).join()
25-
success.get() && resultA.get() == resultB.get()
25+
success.get() && resultA.get() != null && resultA.get() == resultB.get()
2626
}
2727
}
2828

@@ -40,6 +40,6 @@ class JobKWTest : UnitSpec() {
4040
}
4141

4242
init {
43-
testLaws(AsyncLaws.laws(JobKW.asyncContext(EmptyCoroutineContext), JobKW.monadError(EmptyCoroutineContext), EQ, EQ_ERR))
43+
testLaws(AsyncLaws.laws(JobKW.asyncContext(EmptyCoroutineContext), JobKW.monadError(EmptyCoroutineContext), EQ(), EQ(), EQ_ERR))
4444
}
4545
}

kategory-test/src/main/kotlin/kategory/generators/Generators.kt

+26-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ package kategory
22

33
import io.kotlintest.properties.Gen
44

5+
fun <F> genEqAnyLogged() = object : Eq<F> {
6+
val any = Eq.any()
7+
8+
override fun eqv(a: F, b: F): Boolean {
9+
val result = any.eqv(a, b)
10+
if (!result) {
11+
println("$a <---> $b")
12+
}
13+
return result
14+
}
15+
}
16+
517
inline fun <reified F, A> genApplicative(valueGen: Gen<A>, AP: Applicative<F> = applicative<F>()): Gen<HK<F, A>> =
618
object : Gen<HK<F, A>> {
719
override fun generate(): HK<F, A> =
@@ -59,7 +71,18 @@ fun genIntPredicate(): Gen<(Int) -> Boolean> =
5971
fun <B> genOption(genB: Gen<B>): Gen<Option<B>> =
6072
object : Gen<Option<B>> {
6173
val random = genIntSmall()
62-
override fun generate(): Option<B> {
63-
return if (random.generate() % 20 == 0) Option.None else Option.pure(genB.generate())
64-
}
74+
override fun generate(): Option<B> =
75+
if (random.generate() % 20 == 0) Option.None else Option.pure(genB.generate())
76+
}
77+
78+
inline fun <reified E, reified A> genEither(genE: Gen<E>, genA: Gen<A>): Gen<Either<E, A>> =
79+
object : Gen<Either<E, A>> {
80+
override fun generate(): Either<E, A> =
81+
Gen.oneOf(genE, genA).generate().let {
82+
when (it) {
83+
is E -> Either.Left(it)
84+
is A -> Either.Right(it)
85+
else -> throw IllegalStateException("genEither incorrect value $it")
86+
}
87+
}
6588
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package kategory
2+
3+
import io.kotlintest.properties.Gen
4+
import io.kotlintest.properties.forAll
5+
6+
object ApplicativeErrorLaws {
7+
8+
inline fun <reified F> laws(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQERR: Eq<HK<F, Int>>, EQ_EITHER: Eq<HK<F, Either<Throwable, Int>>>, EQ: Eq<HK<F, Int>> = EQERR): List<Law> =
9+
ApplicativeLaws.laws(AP, EQ) + listOf(
10+
Law("Applicative Error Laws: handle", { applicativeErrorHandle(AP, EQERR) }),
11+
Law("Applicative Error Laws: handle with for error", { applicativeErrorHandleWith(AP, EQERR) }),
12+
Law("Applicative Error Laws: handle with for success", { applicativeErrorHandleWithPure(AP, EQERR) }),
13+
Law("Applicative Error Laws: attempt for error", { applicativeErrorAttemptError(AP, EQ_EITHER) }),
14+
Law("Applicative Error Laws: attempt for success", { applicativeErrorAttemptSuccess(AP, EQ_EITHER) }),
15+
Law("Applicative Error Laws: attempt fromEither consistent with pure", { applicativeErrorAttemptFromEitherConsistentWithPure(AP, EQ_EITHER) }),
16+
Law("Applicative Error Laws: catch captures errors", { applicativeErrorCatch(AP, EQERR) })
17+
)
18+
19+
inline fun <reified F> applicativeErrorHandle(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQ: Eq<HK<F, Int>>): Unit =
20+
forAll(genFunctionAToB<Throwable, Int>(Gen.int()), genThrowable(), { f: (Throwable) -> Int, e: Throwable ->
21+
AP.handleError(AP.raiseError<Int>(e), f).equalUnderTheLaw(AP.pure(f(e)), EQ)
22+
})
23+
24+
inline fun <reified F> applicativeErrorHandleWith(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQ: Eq<HK<F, Int>>): Unit =
25+
forAll(genFunctionAToB<Throwable, HK<F, Int>>(genApplicative(Gen.int(), AP)), genThrowable(), { f: (Throwable) -> HK<F, Int>, e: Throwable ->
26+
AP.handleErrorWith(AP.raiseError<Int>(e), f).equalUnderTheLaw(f(e), EQ)
27+
})
28+
29+
inline fun <reified F> applicativeErrorHandleWithPure(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQ: Eq<HK<F, Int>>): Unit =
30+
forAll(genFunctionAToB<Throwable, HK<F, Int>>(genApplicative(Gen.int(), AP)), Gen.int(), { f: (Throwable) -> HK<F, Int>, a: Int ->
31+
AP.handleErrorWith(AP.pure(a), f).equalUnderTheLaw(AP.pure(a), EQ)
32+
})
33+
34+
inline fun <reified F> applicativeErrorAttemptError(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQ: Eq<HK<F, Either<Throwable, Int>>>): Unit =
35+
forAll(genThrowable(), { e: Throwable ->
36+
AP.attempt(AP.raiseError<Int>(e)).equalUnderTheLaw(AP.pure(e.left()), EQ)
37+
})
38+
39+
inline fun <reified F> applicativeErrorAttemptSuccess(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQ: Eq<HK<F, Either<Throwable, Int>>>): Unit =
40+
forAll(Gen.int(), { a: Int ->
41+
AP.attempt(AP.pure(a)).equalUnderTheLaw(AP.pure(a.right()), EQ)
42+
})
43+
44+
inline fun <reified F> applicativeErrorAttemptFromEitherConsistentWithPure(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQ: Eq<HK<F, Either<Throwable, Int>>>): Unit =
45+
forAll(genEither(genThrowable(), Gen.int()), { either: Either<Throwable, Int> ->
46+
AP.attempt(AP.fromEither(either)).equalUnderTheLaw(AP.pure(either), EQ)
47+
})
48+
49+
inline fun <reified F> applicativeErrorCatch(AP: ApplicativeError<F, Throwable> = applicativeError<F, Throwable>(), EQ: Eq<HK<F, Int>>): Unit =
50+
forAll(genEither(genThrowable(), Gen.int()), { either: Either<Throwable, Int> ->
51+
AP.catch({ either.fold({ throw it }, { it }) }).equalUnderTheLaw(either.fold({ AP.raiseError<Int>(it) }, { AP.pure(it) }), EQ)
52+
})
53+
54+
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import io.kotlintest.properties.forAll
55

66
object MonadErrorLaws {
77

8-
inline fun <reified F> laws(M: MonadError<F, Throwable> = monadError<F, Throwable>(), EQERR: Eq<HK<F, Int>>, EQ: Eq<HK<F, Int>> = EQERR): List<Law> =
9-
MonadLaws.laws(M, EQ) + listOf(
8+
inline fun <reified F> laws(M: MonadError<F, Throwable> = monadError<F, Throwable>(), EQERR: Eq<HK<F, Int>>, EQ_EITHER: Eq<HK<F, Either<Throwable, Int>>>, EQ: Eq<HK<F, Int>> = EQERR): List<Law> =
9+
MonadLaws.laws(M, EQ) + ApplicativeErrorLaws.laws(M, EQERR, EQ_EITHER, EQ) + listOf(
1010
Law("Monad Error Laws: left zero", { monadErrorLeftZero(M, EQERR) }),
1111
Law("Monad Error Laws: ensure consistency", { monadErrorEnsureConsistency(M, EQERR) })
1212
)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ fun <B> Option<B>.getOrElse(default: () -> B): B = fold({ default() }, { it })
180180
*
181181
* @param default the default option if this is empty.
182182
*/
183-
fun <A, B : A> Option<B>.orElse(alternative: () -> Option<B>): Option<B> = if (isEmpty) alternative() else this
183+
fun <A, B : A> OptionKind<B>.orElse(alternative: () -> Option<B>): Option<B> = if (ev().isEmpty) alternative() else ev()
184184

185185
fun <A> A.some(): Option<A> = Option.Some(this)
186186

kategory/src/test/kotlin/kategory/data/EitherTTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import org.junit.runner.RunWith
77
class EitherTTest : UnitSpec() {
88
init {
99

10-
testLaws(MonadErrorLaws.laws(EitherT.monadError<IdHK, Throwable>(Id.monad()), Eq.any()))
10+
testLaws(MonadErrorLaws.laws(EitherT.monadError<IdHK, Throwable>(Id.monad()), Eq.any(), Eq.any()))
1111
testLaws(TraverseLaws.laws(EitherT.traverse<IdHK, Int>(), EitherT.applicative(), { EitherT(Id(Either.Right(it))) }, Eq.any()))
1212
testLaws(SemigroupKLaws.laws<EitherTKindPartial<IdHK, Int>>(
1313
EitherT.semigroupK(Id.monad()),

kategory/src/test/kotlin/kategory/data/EitherTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import org.junit.runner.RunWith
1111
class EitherTest : UnitSpec() {
1212
init {
1313

14-
testLaws(MonadErrorLaws.laws(Either.monadError(), Eq.any()))
14+
testLaws(MonadErrorLaws.laws(Either.monadError(), Eq.any(), Eq.any()))
1515
testLaws(TraverseLaws.laws(Either.traverse<Throwable>(), Either.applicative(), { it.right() }, Eq.any()))
1616
testLaws(SemigroupKLaws.laws(
1717
Either.semigroupK(),

kategory/src/test/kotlin/kategory/data/KleisliTest.kt

+8-7
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import org.junit.runner.RunWith
66

77
@RunWith(KTestJUnitRunner::class)
88
class KleisliTest : UnitSpec() {
9-
init {
10-
11-
val me = Kleisli.monadError<TryHK, Int, Throwable>(Try.monadError())
9+
private fun <A> EQ(): Eq<KleisliKind<TryHK, Int, A>> {
10+
return object : Eq<KleisliKind<TryHK, Int, A>> {
11+
override fun eqv(a: KleisliKind<TryHK, Int, A>, b: KleisliKind<TryHK, Int, A>): Boolean =
12+
a.ev().run(1) == b.ev().run(1)
1213

13-
testLaws(MonadErrorLaws.laws(me, object : Eq<KleisliKind<TryHK, Int, Int>> {
14-
override fun eqv(a: KleisliKind<TryHK, Int, Int>, b: KleisliKind<TryHK, Int, Int>): Boolean =
15-
a.ev().run(1) == b.ev().run(1)
14+
}
15+
}
1616

17-
}))
17+
init {
18+
testLaws(MonadErrorLaws.laws(Kleisli.monadError<TryHK, Int, Throwable>(Try.monadError()), EQ(), EQ()))
1819

1920
"andThen should continue sequence" {
2021
val kleisli: Kleisli<IdHK, Int, Int> = Kleisli({ a: Int -> Id(a) })

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,23 @@ class OptionTest : UnitSpec() {
1313
object OptionError : RuntimeException()
1414

1515
init {
16+
val EQ_EITHER: Eq<HK<OptionHK, Either<Throwable, Int>>> = object : Eq<HK<OptionHK, Either<Throwable, Int>>> {
17+
override fun eqv(a: HK<OptionHK, Either<Throwable, Int>>, b: HK<OptionHK, Either<Throwable, Int>>): Boolean =
18+
a.ev().fold(
19+
{ b.ev().fold({ true }, { false }) },
20+
{ eitherA: Either<Throwable, Int> ->
21+
b.ev().fold(
22+
{ false },
23+
{ eitherB: Either<Throwable, Int> ->
24+
eitherA.fold(
25+
{ eitherB.fold({ true /* Ignore the error kind */ }, { false }) },
26+
{ ia -> eitherB.fold({ false }, { ia == it }) })
27+
})
28+
})
29+
}
30+
1631

17-
testLaws(MonadErrorLaws.laws(Option.monadError<Throwable>(OptionError), Eq.any()))
32+
testLaws(MonadErrorLaws.laws(Option.monadError<Throwable>(OptionError), Eq.any(), EQ_EITHER))
1833
testLaws(TraverseLaws.laws(Option.traverse(), Option.monad(), ::Some, Eq.any()))
1934

2035
"fromNullable should work for both null and non-null values of nullable types" {

kategory/src/test/kotlin/kategory/data/TryTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class TryTest : UnitSpec() {
1111

1212
init {
1313

14-
testLaws(MonadErrorLaws.laws(Try.monadError(), Eq.any()))
14+
testLaws(MonadErrorLaws.laws(Try.monadError(), Eq.any(), Eq.any()))
1515
testLaws(TraverseLaws.laws(Try.traverse(), Try.functor(), ::Success, Eq.any()))
1616

1717
"invoke of any should be success" {

0 commit comments

Comments
 (0)