Skip to content

Commit 38f42ea

Browse files
authored
Merge pull request #128 from kategory/paco-foldablelaws
Add laws for Foldable instances
2 parents b8e962f + f97884c commit 38f42ea

File tree

5 files changed

+144
-30
lines changed

5 files changed

+144
-30
lines changed

kategory/src/main/kotlin/kategory/typeclasses/Eq.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ interface Eq<in F> : Typeclass {
77
!eqv(a, b)
88

99
companion object {
10-
fun any(): Eq<Any?> =
11-
EqAny()
10+
inline fun any(): Eq<Any?> =
11+
EqAny
1212

13-
private class EqAny : Eq<Any?> {
13+
object EqAny : Eq<Any?> {
1414
override fun eqv(a: Any?, b: Any?): Boolean =
1515
a == b
1616

kategory/src/main/kotlin/kategory/typeclasses/Foldable.kt

+25-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kategory.Eval.Companion.always
1212
*
1313
* Beyond these it provides many other useful methods related to folding over F<A> values.
1414
*/
15-
interface Foldable<F> : Typeclass {
15+
interface Foldable<in F> : Typeclass {
1616

1717
/**
1818
* Left associative fold on F using the provided function.
@@ -37,7 +37,8 @@ interface Foldable<F> : Typeclass {
3737
*
3838
* Note: will not terminate for infinite-sized collections.
3939
*/
40-
fun <A> size(ml: Monoid<Long>, fa: HK<F, A>): Long = foldMap(ml, fa)({ _ -> 1L })
40+
fun <A> size(ml: Monoid<Long>, fa: HK<F, A>): Long =
41+
foldMap(ml, fa, { _ -> 1L })
4142

4243
/**
4344
* Fold implemented using the given Monoid<A> instance.
@@ -53,8 +54,8 @@ interface Foldable<F> : Typeclass {
5354
/**
5455
* Fold implemented by mapping A values into B and then combining them using the given Monoid<B> instance.
5556
*/
56-
fun <A, B> foldMap(mb: Monoid<B>, fa: HK<F, A>): (f: (A) -> B) -> B =
57-
{ f: (A) -> B -> foldL(fa, mb.empty(), { b, a -> mb.combine(b, f(a)) }) }
57+
fun <A, B> foldMap(mb: Monoid<B>, fa: HK<F, A>, f: (A) -> B): B =
58+
foldL(fa, mb.empty(), { b, a -> mb.combine(b, f(a)) })
5859

5960
/**
6061
* Left associative monadic folding on F.
@@ -63,18 +64,16 @@ interface Foldable<F> : Typeclass {
6364
* Certain structures are able to implement this in such a way that folds can be short-circuited (not traverse the
6465
* entirety of the structure), depending on the G result produced at a given step.
6566
*/
66-
fun <G, A, B> foldM(MG: Monad<G>, fa: HK<F, A>, z: B, f: (B, A) -> HK<G, B>): HK<G, B> {
67-
return foldL(fa, MG.pure(z), { gb, a -> MG.flatMap(gb) { f(it, a) } })
68-
}
67+
fun <G, A, B> foldM(MG: Monad<G>, fa: HK<F, A>, z: B, f: (B, A) -> HK<G, B>): HK<G, B> =
68+
foldL(fa, MG.pure(z), { gb, a -> MG.flatMap(gb) { f(it, a) } })
6969

7070
/**
7171
* Monadic folding on F by mapping A values to G<B>, combining the B values using the given Monoid<B> instance.
7272
*
7373
* Similar to foldM, but using a Monoid<B>.
7474
*/
75-
fun <G, A, B> foldMapM(MG: Monad<G>, bb: Monoid<B>, fa: HK<F, A>, f: (A) -> HK<G, B>) : HK<G, B> {
76-
return foldM(MG, fa, bb.empty(), { b, a -> MG.map(f(a)) { bb.combine(b, it) } })
77-
}
75+
fun <G, A, B> foldMapM(MG: Monad<G>, bb: Monoid<B>, fa: HK<F, A>, f: (A) -> HK<G, B>) : HK<G, B> =
76+
foldM(MG, fa, bb.empty(), { b, a -> MG.map(f(a)) { bb.combine(b, it) } })
7877

7978
/**
8079
* Traverse F<A> using Applicative<G>.
@@ -95,28 +94,38 @@ interface Foldable<F> : Typeclass {
9594
fun <G, A> sequence_(ag: Applicative<G>, fga: HK<F, HK<G, A>>): HK<G, Unit> =
9695
traverse_(ag, fga, { it })
9796

97+
/**
98+
* Find the first element matching the predicate, if one exists.
99+
*/
100+
fun <A> find(fa: HK<F, A>, f: (A) -> Boolean): Option<A> =
101+
foldR(fa, Eval.now<Option<A>>(Option.None), { a, lb ->
102+
if (f(a)) Eval.now(Option.Some(a)) else lb
103+
}).value()
104+
98105
/**
99106
* Check whether at least one element satisfies the predicate.
100107
*
101108
* If there are no elements, the result is false.
102109
*/
103-
fun <A> exists(fa: HK<F, A>): (p: (A) -> Boolean) -> Boolean =
104-
{ p: (A) -> Boolean -> foldR(fa, Eval.False, { a, lb -> if (p(a)) Eval.True else lb }).value() }
110+
fun <A> exists(fa: HK<F, A>, p: (A) -> Boolean): Boolean =
111+
foldR(fa, Eval.False, { a, lb -> if (p(a)) Eval.True else lb }).value()
105112

106113
/**
107114
* Check whether all elements satisfy the predicate.
108115
*
109116
* If there are no elements, the result is true.
110117
*/
111-
fun <A> forall(fa: HK<F, A>): (p: (A) -> Boolean) -> Boolean =
112-
{ p: (A) -> Boolean -> foldR(fa, Eval.True, { a, lb -> if (p(a)) lb else Eval.False }).value() }
118+
fun <A> forall(fa: HK<F, A>, p: (A) -> Boolean): Boolean =
119+
foldR(fa, Eval.True, { a, lb -> if (p(a)) lb else Eval.False }).value()
113120

114121
/**
115122
* Returns true if there are no elements. Otherwise false.
116123
*/
117-
fun <A> isEmpty(fa: HK<F, A>): Boolean = foldR(fa, Eval.True, { _, _ -> Eval.False }).value()
124+
fun <A> isEmpty(fa: HK<F, A>): Boolean =
125+
foldR(fa, Eval.True, { _, _ -> Eval.False }).value()
118126

119-
fun <A> nonEmpty(fa: HK<F, A>): Boolean = !isEmpty(fa)
127+
fun <A> nonEmpty(fa: HK<F, A>): Boolean =
128+
!isEmpty(fa)
120129

121130
companion object {
122131
fun <A, B> iterateRight(it: Iterator<A>, lb: Eval<B>): (f: (A, Eval<B>) -> Eval<B>) -> Eval<B> = {

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

+35-10
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,43 @@ package kategory
22

33
import io.kotlintest.properties.Gen
44

5-
inline fun <reified F, A> genApplicative(valueGen: Gen<A>, AP: Applicative<F> = applicative<F>()): Gen<HK<F, A>> = object : Gen<HK<F, A>> {
6-
override fun generate(): HK<F, A> = AP.pure(valueGen.generate())
7-
}
5+
inline fun <reified F, A> genApplicative(valueGen: Gen<A>, AP: Applicative<F> = applicative<F>()): Gen<HK<F, A>> =
6+
object : Gen<HK<F, A>> {
7+
override fun generate(): HK<F, A> =
8+
AP.pure(valueGen.generate())
9+
}
810

9-
fun <A, B> genFunctionAToB(genB: Gen<B>): Gen<(A) -> B> = object : Gen<(A) -> B> {
10-
override fun generate(): (A) -> B {
11-
val v = genB.generate()
12-
return { a -> v }
13-
}
14-
}
11+
fun <A, B> genFunctionAToB(genB: Gen<B>): Gen<(A) -> B> =
12+
object : Gen<(A) -> B> {
13+
override fun generate(): (A) -> B {
14+
val v = genB.generate()
15+
return { _ -> v }
16+
}
17+
}
1518

1619
fun genThrowable(): Gen<Throwable> = object : Gen<Throwable> {
1720
override fun generate(): Throwable =
1821
Gen.oneOf(listOf(RuntimeException(), NoSuchElementException(), IllegalArgumentException())).generate()
19-
}
22+
}
23+
24+
inline fun <F, A> genConstructor(valueGen: Gen<A>, crossinline cf: (A) -> HK<F, A>): Gen<HK<F, A>> =
25+
object : Gen<HK<F, A>> {
26+
override fun generate(): HK<F, A> =
27+
cf(valueGen.generate())
28+
}
29+
30+
fun genIntSmall(): Gen<Int> =
31+
Gen.oneOf(Gen.negativeIntegers(), Gen.choose(0, Int.MAX_VALUE / 10000))
32+
33+
fun genIntPredicate(): Gen<(Int) -> Boolean> =
34+
Gen.int().let { gen ->
35+
/* If you ever see two zeros in a row please contact the maintainers for a pat in the back */
36+
val num = gen.generate().let { if (it == 0) gen.generate() else it }
37+
val absNum = Math.abs(num)
38+
Gen.oneOf(listOf<(Int) -> Boolean>(
39+
{ it > num },
40+
{ it <= num },
41+
{ it % absNum == 0 },
42+
{ it % absNum == absNum - 1 })
43+
)
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package kategory
2+
3+
import io.kotlintest.properties.Gen
4+
import io.kotlintest.properties.forAll
5+
6+
object FoldableLaws {
7+
inline fun <reified F> laws(FF: Foldable<F> = foldable<F>(), crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>): List<Law> =
8+
listOf(
9+
Law("Foldable Laws: Left fold consistent with foldMap", { leftFoldConsistentWithFoldMap(FF, cf, EQ) }),
10+
Law("Foldable Laws: Right fold consistent with foldMap", { rightFoldConsistentWithFoldMap(FF, cf, EQ) }),
11+
Law("Foldable Laws: Exists is consistent with find", { existsConsistentWithFind(FF, cf, EQ) }),
12+
Law("Foldable Laws: Exists is lazy", { existsIsLazy(FF, cf, EQ) }),
13+
Law("Foldable Laws: ForAll is lazy", { forAllIsLazy(FF, cf, EQ) }),
14+
Law("Foldable Laws: ForAll consistent with exists", { forallConsistentWithExists(FF, cf) }),
15+
Law("Foldable Laws: ForAll returns true if isEmpty", { forallReturnsTrueIfEmpty(FF, cf) }),
16+
Law("Foldable Laws: FoldM for Id is equivalent to fold left", { foldMIdIsFoldL(FF, cf, EQ) })
17+
)
18+
19+
inline fun <reified F> leftFoldConsistentWithFoldMap(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
20+
forAll(genFunctionAToB<Int, Int>(genIntSmall()), genConstructor(genIntSmall(), cf), { f: (Int) -> Int, fa: HK<F, Int> ->
21+
FF.foldMap(IntMonoid, fa, f).equalUnderTheLaw(FF.foldL(fa, IntMonoid.empty(), { acc, a -> IntMonoid.combine(acc, f(a)) }), EQ)
22+
})
23+
24+
inline fun <reified F> rightFoldConsistentWithFoldMap(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
25+
forAll(genFunctionAToB<Int, Int>(genIntSmall()), genConstructor(genIntSmall(), cf), { f: (Int) -> Int, fa: HK<F, Int> ->
26+
FF.foldMap(IntMonoid, fa, f).equalUnderTheLaw(FF.foldR(fa, Eval.later { IntMonoid.empty() }, { a, lb: Eval<Int> -> lb.map { IntMonoid.combine(f(a), it) } }).value(), EQ)
27+
})
28+
29+
inline fun <reified F> existsConsistentWithFind(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
30+
forAll(genIntPredicate(), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
31+
FF.exists(fa, f).equalUnderTheLaw(FF.find(fa, f).fold({ false }, { true }), EQ)
32+
})
33+
34+
inline fun <reified F> existsIsLazy(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
35+
forAll(genConstructor(Gen.int(), cf), { fa: HK<F, Int> ->
36+
val sideEffect = SideEffect()
37+
FF.exists(fa, { _ ->
38+
sideEffect.increment()
39+
true
40+
})
41+
val expected = if (FF.isEmpty(fa)) 0 else 1
42+
sideEffect.counter.equalUnderTheLaw(expected, EQ)
43+
})
44+
45+
inline fun <reified F> forAllIsLazy(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
46+
forAll(genConstructor(Gen.int(), cf), { fa: HK<F, Int> ->
47+
val sideEffect = SideEffect()
48+
FF.forall(fa, { _ ->
49+
sideEffect.increment()
50+
true
51+
})
52+
val expected = if (FF.isEmpty(fa)) 0 else 1
53+
sideEffect.counter.equalUnderTheLaw(expected, EQ)
54+
})
55+
56+
inline fun <reified F> forallConsistentWithExists(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>) =
57+
forAll(genIntPredicate(), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
58+
if (FF.forall(fa, f)) {
59+
val negationExists = FF.exists(fa, { a -> !(f(a)) })
60+
// if p is true for all elements, then there cannot be an element for which
61+
// it does not hold.
62+
!negationExists &&
63+
// if p is true for all elements, then either there must be no elements
64+
// or there must exist an element for which it is true.
65+
(FF.isEmpty(fa) || FF.exists(fa, f))
66+
} else true
67+
})
68+
69+
inline fun <reified F> forallReturnsTrueIfEmpty(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>) =
70+
forAll(genIntPredicate(), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
71+
!FF.isEmpty(fa) || FF.forall(fa, f)
72+
})
73+
74+
inline fun <reified F> foldMIdIsFoldL(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
75+
forAll(genFunctionAToB<Int, Int>(genIntSmall()), genConstructor(genIntSmall(), cf), { f: (Int) -> Int, fa: HK<F, Int> ->
76+
val foldL: Int = FF.foldL(fa, IntMonoid.empty(), { acc, a -> IntMonoid.combine(acc, f(a)) })
77+
val foldM: Int = FF.foldM(Id, fa, IntMonoid.empty(), { acc, a -> Id(IntMonoid.combine(acc, f(a))) } ).value()
78+
foldM.equalUnderTheLaw(foldL, EQ)
79+
})
80+
}

kategory/src/test/kotlin/kategory/laws/FunctorLaws.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ object FunctorLaws {
88
inline fun <reified F> laws(AP: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>>): List<Law> =
99
listOf(
1010
Law("Functor Laws: Covariant Identity", { covariantIdentity(AP, EQ) }),
11-
Law("Functor: Covariant Composition", { covariantComposition(AP, EQ) })
11+
Law("Functor Laws: Covariant Composition", { covariantComposition(AP, EQ) })
1212
)
1313

1414
inline fun <reified F> covariantIdentity(AP: Applicative<F> = applicative<F>(), EQ: Eq<HK<F, Int>> = Eq.any()): Unit =

0 commit comments

Comments
 (0)