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

Initial prism impl #258

Merged
merged 2 commits into from
Sep 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions kategory-optics/src/main/kotlin/kategory/optics/Lens.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
package kategory
package kategory.optics

import kategory.Either
import kategory.Functor
import kategory.HK
import kategory.Option
import kategory.Tuple2
import kategory.functor
import kategory.toT

/**
* A [Lens] can be seen as a pair of functions `get: (A) -> B` and `set: (B) -> (A) -> A`
Expand Down Expand Up @@ -59,25 +67,25 @@ abstract class Lens<A, B> {
/**
* Pair two disjoint [Lens]
*/
fun <C, D> split(other: Lens<C, D>): Lens<Pair<A, C>, Pair<B, D>> = Lens(
{ (a, c) -> get(a) to other.get(c) },
{ (b, d) -> { (a, c) -> set(b)(a) to other.set(d)(c) } }
fun <C, D> split(other: Lens<C, D>): Lens<Tuple2<A, C>, Tuple2<B, D>> = Lens(
{ (a, c) -> get(a) toT other.get(c) },
{ (b, d) -> { (a, c) -> set(b)(a) toT other.set(d)(c) } }
)

/**
* Convenience method to create a pair of the target and a type C
*/
fun <C> first(): Lens<Pair<A, C>, Pair<B, C>> = Lens(
{ (a, c) -> get(a) to c },
{ (b, c) -> { (a, _) -> set(b)(a) to c } }
fun <C> first(): Lens<Tuple2<A, C>, Tuple2<B, C>> = Lens(
{ (a, c) -> get(a) toT c },
{ (b, c) -> { (a, _) -> set(b)(a) toT c } }
)

/**
* Convenience method to create a pair of a type C and the target
*/
fun <C> second(): Lens<Pair<C, A>, Pair<C, B>> = Lens(
{ (c, a) -> c to get(a) },
{ (c, b) -> { (_, a) -> c to set(b)(a) } }
fun <C> second(): Lens<Tuple2<C, A>, Tuple2<C, B>> = Lens(
{ (c, a) -> c toT get(a) },
{ (c, b) -> { (_, a) -> c toT set(b)(a) } }
)

/**
Expand Down
143 changes: 143 additions & 0 deletions kategory-optics/src/main/kotlin/kategory/optics/Prism.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package kategory.optics

import kategory.Applicative
import kategory.Either
import kategory.HK
import kategory.Option
import kategory.Tuple2
import kategory.flatMap
import kategory.identity
import kategory.left
import kategory.right
import kategory.toT

/**
* A [Prism] can be seen as a pair of functions: `reverseGet : B -> A` and `getOrModify: A -> Either<A,B>`
*
* - `reverseGet : B -> A` get the source type of a [Prism]
* - `getOrModify: A -> Either<A,B>` get the target of a [Prism] or return the original value
*
* It encodes the relation between a Sum or CoProduct type (sealed class) and one of its element.
*
* @param A the source of a [Prism]
* @param B the target of a [Prism]
* @property getOrModify from an `B` we can produce an `A`
* @property reverseGet get the target of a [Prism] or return the original value
* @constructor Creates a Lens of type `A` with target `B`
*/
abstract class Prism<A, B> {

abstract fun getOrModify(a: A): Either<A, B>
abstract fun reverseGet(b: B): A

companion object {
operator fun <A,B> invoke(getOrModify: (A) -> Either<A, B>, reverseGet: (B) -> A) = object : Prism<A,B>() {
override fun getOrModify(a: A): Either<A, B> = getOrModify(a)

override fun reverseGet(b: B): A = reverseGet(b)
}
}

/**
* Get the target or nothing if `A` does not match the target
*/
fun getOption(a: A): Option<B> = getOrModify(a).toOption()

/**
* Modify the target of a [Prism] with an Applicative function
*/
inline fun <reified F> modifyF(FA: Applicative<F> = kategory.applicative(), crossinline f: (B) -> HK<F, B>, a: A): HK<F, A> = getOrModify(a).fold(
{ FA.pure(it) },
{ FA.map(f(it), this::reverseGet) }
)

/**
* Modify the target of a [Prism] with a function
*/
inline fun modify(crossinline f: (B) -> B): (A) -> A = {
getOrModify(it).fold(::identity, { reverseGet(f(it)) })
}

/**
* Modify the target of a [Prism] with a function
*/
inline fun modifyOption(crossinline f: (B) -> B): (A) -> Option<A> = { getOption(it).map { b -> reverseGet(f(b)) } }

/**
* Set the target of a [Prism] with a value
*/
fun set(b: B): (A) -> A = modify { b }

infix fun <C> composePrism(other: Prism<B, C>): Prism<A, C> = Prism(
{ a -> getOrModify(a).flatMap { b: B -> other.getOrModify(b).bimap({ set(it)(a) }, ::identity) } },
{ reverseGet(other.reverseGet(it)) }
)

/**
* Set the target of a [Prism] with a value
*/
fun setOption(b: B): (A) -> Option<A> = modifyOption { b }

/**
* Check if there is a target
*/
fun isNotEmpty(a: A): Boolean = getOption(a).isDefined

/**
* Check if there is no target
*/
fun isEmpty(a: A): Boolean = !isNotEmpty(a)

/**
* Find if the target satisfies the predicate
*/
inline fun find(crossinline p: (B) -> Boolean): (A) -> Option<B> = { getOption(it).flatMap { if (p(it)) Option.Some(it) else Option.None } }

/**
* Check if there is a target and it satisfies the predicate
*/
inline fun exist(crossinline p: (B) -> Boolean): (A) -> Boolean = { getOption(it).fold({ false }, p) }

/**
* Check if there is no target or the target satisfies the predicate
*/
inline fun all(crossinline p: (B) -> Boolean): (A) -> Boolean = { getOption(it).fold({ true }, p) }

/**
* Convenience method to create a product of the target and a type C
*/
fun <C> first(): Prism<Tuple2<A, C>, Tuple2<B, C>> = Prism(
{ (a, c) -> getOrModify(a).bimap({ it toT c }, { it toT c }) },
{ (b, c) -> reverseGet(b) toT c }
)

/**
* Convenience method to create a product of a type C and the target
*/
fun <C> second(): Prism<Tuple2<C, A>, Tuple2<C, B>> = Prism(
{ (c, a) -> getOrModify(a).bimap({ c toT it }, { c toT it }) },
{ (c, b) -> c toT reverseGet(b) }
)

}

/**
* Convenience method to create a sum of the target and a type C
*/
fun <A, B, C> Prism<A, B>.left(): Prism<Either<A, C>, Either<B, C>> = Prism(
{ it.fold({ a -> getOrModify(a).bimap({ it.left() }, { it.left() }) }, { c -> Either.Right(c.right()) }) },
{
when (it) {
is Either.Left<B, C> -> Either.Left(reverseGet(it.a))
is Either.Right<B, C> -> Either.Right(it.b)
}
}
)

/**
* Convenience method to create a sum of a type C and the target
*/
fun <A, B, C> Prism<A, B>.right(): Prism<Either<C, A>, Either<C, B>> = Prism(
{ it.fold({ c -> Either.Right(c.left()) }, { a -> getOrModify(a).bimap({ it.right() }, { it.right() }) }) },
{ it.map(this::reverseGet) }
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import io.kotlintest.properties.forAll
import kategory.Applicative
import kategory.Eq
import kategory.Law
import kategory.Lens
import kategory.compose
import kategory.exists
import kategory.identity
Expand Down
11 changes: 6 additions & 5 deletions kategory-optics/src/test/kotlin/kategory/optics/LensTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import io.kotlintest.KTestJUnitRunner
import io.kotlintest.properties.Gen
import io.kotlintest.properties.forAll
import kategory.Eq
import kategory.Lens
import kategory.Option
import kategory.Tuple2
import kategory.UnitSpec
import kategory.applicative
import kategory.genFunctionAToB
import kategory.left
import kategory.right
import kategory.toT
import org.junit.runner.RunWith

@RunWith(KTestJUnitRunner::class)
Expand Down Expand Up @@ -58,23 +59,23 @@ class LensTest : UnitSpec() {
}

"Pairing two disjoint lenses should yield a pair of their results" {
val spiltLens: Lens<Pair<Token, User>, Pair<String, Token>> = tokenLens.split(userLens)
val spiltLens: Lens<Tuple2<Token, User>, Tuple2<String, Token>> = tokenLens.split(userLens)
forAll(TokenGen, UserGen, { token: Token, user: User ->
spiltLens.get(token to user) == token.value to user.token
spiltLens.get(token toT user) == token.value toT user.token
})
}

"Creating a first pair with a type should result in the target to value" {
val first = tokenLens.first<Int>()
forAll(TokenGen, Gen.int(), { token: Token, int: Int ->
first.get(token to int) == token.value to int
first.get(token toT int) == token.value toT int
})
}

"Creating a second pair with a type should result in the value target" {
val first = tokenLens.second<Int>()
forAll(Gen.int(), TokenGen, { int: Int, token: Token ->
first.get(int to token) == int to token.value
first.get(int toT token) == int toT token.value
})
}
}
Expand Down
53 changes: 53 additions & 0 deletions kategory-optics/src/test/kotlin/kategory/optics/PrismLaws.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kategory.optics

import io.kotlintest.properties.Gen
import io.kotlintest.properties.forAll
import kategory.Applicative
import kategory.Eq
import kategory.Law
import kategory.compose
import kategory.exists
import kategory.identity

object PrismLaws {

inline fun <A, B, reified F> laws(prism: Prism<A, B>, aGen: Gen<A>, bGen: Gen<B>, funcGen: Gen<(B) -> B>, EQA: Eq<A>, EQB: Eq<B>, FA: Applicative<F>): List<Law> = listOf(
Law("Prism law: partial round trip one way", { partialRoundTripOneWay(prism, aGen, EQA) }),
Law("Prism law: round trip other way", { roundTripOtherWay(prism, bGen, EQB) }),
Law("Prism law: modify identity", { modifyIdentity(prism, aGen, EQA) }),
Law("Prism law: compose modify", { composeModify(prism, aGen, funcGen, EQA) }),
Law("Prism law: consistent set modify", { consistentSetModify(prism, aGen, bGen, EQA) }),
Law("Prism law: consistent get option modify id", { consistentGetOptionModifyId(prism, aGen, FA, EQB) })
)

fun <A, B> partialRoundTripOneWay(prism: Prism<A, B>, aGen: Gen<A>, EQA: Eq<A>): Unit =
forAll(aGen, { a ->
EQA.eqv(prism.getOrModify(a).fold(::identity, prism::reverseGet), a)
})

fun <A, B> roundTripOtherWay(prism: Prism<A, B>, bGen: Gen<B>, EQB: Eq<B>): Unit =
forAll(bGen, { b ->
prism.getOption(prism.reverseGet(b)).exists { EQB.eqv(it, b) }
})

fun <A, B> modifyIdentity(prism: Prism<A, B>, aGen: Gen<A>, EQA: Eq<A>): Unit =
forAll(aGen, { a ->
EQA.eqv(prism.modify(::identity)(a), a)
})

fun <A, B> composeModify(prism: Prism<A, B>, aGen: Gen<A>, funcGen: Gen<(B) -> B>, EQA: Eq<A>): Unit =
forAll(aGen, funcGen, funcGen, { a, f, g ->
EQA.eqv(prism.modify(g)(prism.modify(f)(a)), prism.modify(g compose f)(a))
})

fun <A, B> consistentSetModify(prism: Prism<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>): Unit =
forAll(aGen, bGen, { a, b ->
EQA.eqv(prism.set(b)(a), prism.modify { b }(a))
})

inline fun <A, B, reified F> consistentGetOptionModifyId(prism: Prism<A, B>, aGen: Gen<A>, FA: Applicative<F>, EQB: Eq<B>): Unit =
forAll(aGen, { a ->
prism.modifyF(FA, { FA.pure(it) }, a).exists { prism.getOption(it).exists { b -> prism.getOption(a).exists { EQB.eqv(b, it) } } }
})

}
Loading