-
Notifications
You must be signed in to change notification settings - Fork 448
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
+144
−30
Merged
Changes from 8 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0c019aa
Add new generators
341b8e1
Improve Eq
050efc8
Fix variance in Foldable. Fix pre-curried functions in Foldable.
0f8fca9
Fix typo
14f7590
Add foldable laws
329d3ff
Merge branch 'master' into paco-foldablelaws
pakoito 9955d98
Merge branch 'master' of https://github.com/FineCinnamon/Katz into pa…
8355908
Merge remote-tracking branch 'origin/paco-foldablelaws' into paco-fol…
562ea47
Fix mod functions. Fix formatting.
f97884c
Fix formatting
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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. | ||
|
@@ -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. | ||
|
@@ -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> = | ||
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> = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>. | ||
|
@@ -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> = { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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()
?There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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