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 laws for Foldable instances #128

Merged
merged 10 commits into from
Jul 16, 2017
6 changes: 3 additions & 3 deletions kategory/src/main/kotlin/kategory/typeclasses/Eq.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ interface Eq<in F> : Typeclass {
!eqv(a, b)

companion object {
fun any(): Eq<Any?> =
EqAny()
inline fun any(): Eq<Any?> =
EqAny

private class EqAny : Eq<Any?> {
object EqAny : Eq<Any?> {
override fun eqv(a: Any?, b: Any?): Boolean =
a == b

Expand Down
41 changes: 25 additions & 16 deletions kategory/src/main/kotlin/kategory/typeclasses/Foldable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import kategory.Eval.Companion.always
*
* Beyond these it provides many other useful methods related to folding over F<A> values.
*/
interface Foldable<F> : Typeclass {
interface Foldable<in F> : Typeclass {

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

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

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

Choose a reason for hiding this comment

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

should this contain the default param value = monad()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sadly it's not possible

screen shot 2017-07-17 at 12 26 42 am

Copy link
Member

Choose a reason for hiding this comment

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

what if it is expressed as an extension function?

Copy link
Member Author

@pakoito pakoito Jul 16, 2017

Choose a reason for hiding this comment

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

That might work. I'm starting to seriously consider that we'd remove several headaches if we were to express all functions bar the pure virtual ones as extensions.

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've added them to this PR #129

foldL(fa, MG.pure(z), { gb, a -> MG.flatMap(gb) { f(it, a) } })

/**
* Monadic folding on F by mapping A values to G<B>, combining the B values using the given Monoid<B> instance.
*
* Similar to foldM, but using a Monoid<B>.
*/
fun <G, A, B> foldMapM(MG: Monad<G>, bb: Monoid<B>, fa: HK<F, A>, f: (A) -> HK<G, B>) : HK<G, B> {
return foldM(MG, fa, bb.empty(), { b, a -> MG.map(f(a)) { bb.combine(b, it) } })
}
fun <G, A, B> foldMapM(MG: Monad<G>, bb: Monoid<B>, fa: HK<F, A>, f: (A) -> HK<G, B>) : HK<G, B> =
Copy link
Member

Choose a reason for hiding this comment

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

Same for monoid

foldM(MG, fa, bb.empty(), { b, a -> MG.map(f(a)) { bb.combine(b, it) } })

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

/**
* Find the first element matching the predicate, if one exists.
*/
fun <A> find(fa: HK<F, A>, f: (A) -> Boolean): Option<A> =
foldR(fa, Eval.now<Option<A>>(Option.None), { a, lb ->
if (f(a)) Eval.now(Option.Some(a)) else lb
}).value()

/**
* Check whether at least one element satisfies the predicate.
*
* If there are no elements, the result is false.
*/
fun <A> exists(fa: HK<F, A>): (p: (A) -> Boolean) -> Boolean =
{ p: (A) -> Boolean -> foldR(fa, Eval.False, { a, lb -> if (p(a)) Eval.True else lb }).value() }
fun <A> exists(fa: HK<F, A>, p: (A) -> Boolean): Boolean =
foldR(fa, Eval.False, { a, lb -> if (p(a)) Eval.True else lb }).value()

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

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

fun <A> nonEmpty(fa: HK<F, A>): Boolean = !isEmpty(fa)
fun <A> nonEmpty(fa: HK<F, A>): Boolean =
!isEmpty(fa)

companion object {
fun <A, B> iterateRight(it: Iterator<A>, lb: Eval<B>): (f: (A, Eval<B>) -> Eval<B>) -> Eval<B> = {
Expand Down
42 changes: 32 additions & 10 deletions kategory/src/test/kotlin/kategory/generators/Generators.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,40 @@ package kategory

import io.kotlintest.properties.Gen

inline fun <reified F, A> genApplicative(valueGen: Gen<A>, AP: Applicative<F> = applicative<F>()): Gen<HK<F, A>> = object : Gen<HK<F, A>> {
override fun generate(): HK<F, A> = AP.pure(valueGen.generate())
}
inline fun <reified F, A> genApplicative(valueGen: Gen<A>, AP: Applicative<F> = applicative<F>()): Gen<HK<F, A>> =
object : Gen<HK<F, A>> {
override fun generate(): HK<F, A> = AP.pure(valueGen.generate())
}

fun <A, B> genFunctionAToB(genB: Gen<B>): Gen<(A) -> B> = object : Gen<(A) -> B> {
override fun generate(): (A) -> B {
val v = genB.generate()
return { a -> v }
}
}
fun <A, B> genFunctionAToB(genB: Gen<B>): Gen<(A) -> B> =
object : Gen<(A) -> B> {
override fun generate(): (A) -> B {
val v = genB.generate()
return { _ -> v }
}
}

fun genThrowable(): Gen<Throwable> = object : Gen<Throwable> {
override fun generate(): Throwable =
Gen.oneOf(listOf(RuntimeException(), NoSuchElementException(), IllegalArgumentException())).generate()
}
}

inline fun <F, A> genConstructor(valueGen: Gen<A>, crossinline cf: (A) -> HK<F, A>): Gen<HK<F, A>> =
object : Gen<HK<F, A>> {
override fun generate(): HK<F, A> = cf(valueGen.generate())
}

fun genIntSmall(): Gen<Int> =
Gen.oneOf(Gen.negativeIntegers(), Gen.choose(0, Int.MAX_VALUE / 10000))

fun genIntPredicate(): Gen<(Int) -> Boolean> =
Gen.int().let { gen ->
/* If you ever see two 0s in a row please contact the maintainers for a pat in the back */
val num = gen.generate().let { if (it == 0) gen.generate() else it }
Gen.oneOf(listOf<(Int) -> Boolean>(
{ it > num },
{ it <= num },
{ it % num == 0 },
{ it % num == num - 1 })
)
}
80 changes: 80 additions & 0 deletions kategory/src/test/kotlin/kategory/laws/FoldableLaws.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package kategory

import io.kotlintest.properties.Gen
import io.kotlintest.properties.forAll

object FoldableLaws {
inline fun <reified F> laws(FF: Foldable<F> = foldable<F>(), crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>): List<Law> =
listOf(
Law("Foldable Laws: Left fold consistent with foldMap", { leftFoldConsistentWithFoldMap(FF, cf, EQ) }),
Law("Foldable Laws: Right fold consistent with foldMap", { rightFoldConsistentWithFoldMap(FF, cf, EQ) }),
Law("Foldable Laws: Exists is consistent with find", { existsConsistentWithFind(FF, cf, EQ) }),
Law("Foldable Laws: Exists is lazy", { existsIsLazy(FF, cf, EQ) }),
Law("Foldable Laws: ForAll is lazy", { forAllIsLazy(FF, cf, EQ) }),
Law("Foldable Laws: ForAll consistent with exists", { forallConsistentWithExists(FF, cf) }),
Law("Foldable Laws: ForAll returns true if isEmpty", { forallReturnsTrueIfEmpty(FF, cf) }),
Law("Foldable Laws: FoldM for Id is equivalent to fold left", { foldMIdIsFoldL(FF, cf, EQ) })
)

inline fun <reified F> leftFoldConsistentWithFoldMap(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
forAll(genFunctionAToB<Int, Int>(genIntSmall()), genConstructor(genIntSmall(), cf), { f: (Int) -> Int, fa: HK<F, Int> ->
FF.foldMap(IntMonoid, fa, f).equalUnderTheLaw(FF.foldL(fa, IntMonoid.empty(), { acc, a -> IntMonoid.combine(acc, f(a)) }), EQ)
})

inline fun <reified F> rightFoldConsistentWithFoldMap(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
forAll(genFunctionAToB<Int, Int>(genIntSmall()), genConstructor(genIntSmall(), cf), { f: (Int) -> Int, fa: HK<F, Int> ->
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)
})

inline fun <reified F> existsConsistentWithFind(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
forAll(genIntPredicate(), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
FF.exists(fa, f).equalUnderTheLaw(FF.find(fa, f).fold({ false }, { true }), EQ)
})

inline fun <reified F> existsIsLazy(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
forAll(genConstructor(Gen.int(), cf), { fa: HK<F, Int> ->
val sideEffect = SideEffect()
FF.exists(fa, { _ ->
sideEffect.increment()
true
})
val expected = if (FF.isEmpty(fa)) 0 else 1
sideEffect.counter.equalUnderTheLaw(expected, EQ)
})

inline fun <reified F> forAllIsLazy(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
forAll(genConstructor(Gen.int(), cf), { fa: HK<F, Int> ->
val sideEffect = SideEffect()
FF.forall(fa, { _ ->
sideEffect.increment()
true
})
val expected = if (FF.isEmpty(fa)) 0 else 1
sideEffect.counter.equalUnderTheLaw(expected, EQ)
})

inline fun <reified F> forallConsistentWithExists(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>) =
forAll(genIntPredicate(), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
if (FF.forall(fa, f)) {
val negationExists = FF.exists(fa, { a -> !(f(a)) })
// if p is true for all elements, then there cannot be an element for which
// it does not hold.
!negationExists &&
// if p is true for all elements, then either there must be no elements
// or there must exist an element for which it is true.
(FF.isEmpty(fa) || FF.exists(fa, f))
} else true
})

inline fun <reified F> forallReturnsTrueIfEmpty(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>) =
forAll(genIntPredicate(), genConstructor(Gen.int(), cf), { f: (Int) -> Boolean, fa: HK<F, Int> ->
!FF.isEmpty(fa) || FF.forall(fa, f)
})

inline fun <reified F> foldMIdIsFoldL(FF: Foldable<F>, crossinline cf: (Int) -> HK<F, Int>, EQ: Eq<Any?>) =
forAll(genFunctionAToB<Int, Int>(genIntSmall()), genConstructor(genIntSmall(), cf), { f: (Int) -> Int, fa: HK<F, Int> ->
val foldL: Int = FF.foldL(fa, IntMonoid.empty(), { acc, a -> IntMonoid.combine(acc, f(a)) })
val foldM: Int = FF.foldM(Id, fa, IntMonoid.empty(), { acc, a -> Id(IntMonoid.combine(acc, f(a))) } ).value()
foldM.equalUnderTheLaw(foldL, EQ)
})
}
2 changes: 1 addition & 1 deletion kategory/src/test/kotlin/kategory/laws/FunctorLaws.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ object FunctorLaws {
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, EQ) }),
Law("Functor: Covariant Composition", { covariantComposition(AP, EQ) })
Law("Functor Laws: Covariant Composition", { covariantComposition(AP, EQ) })
)

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