Skip to content

Commit 751f25a

Browse files
authored
Merge pull request #258 from nomisRev/simon-prism
Initial prism impl
2 parents 0e77607 + 2048a8c commit 751f25a

File tree

6 files changed

+361
-16
lines changed

6 files changed

+361
-16
lines changed

kategory-optics/src/main/kotlin/kategory/optics/Lens.kt

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
package kategory
1+
package kategory.optics
2+
3+
import kategory.Either
4+
import kategory.Functor
5+
import kategory.HK
6+
import kategory.Option
7+
import kategory.Tuple2
8+
import kategory.functor
9+
import kategory.toT
210

311
/**
412
* A [Lens] can be seen as a pair of functions `get: (A) -> B` and `set: (B) -> (A) -> A`
@@ -59,25 +67,25 @@ abstract class Lens<A, B> {
5967
/**
6068
* Pair two disjoint [Lens]
6169
*/
62-
fun <C, D> split(other: Lens<C, D>): Lens<Pair<A, C>, Pair<B, D>> = Lens(
63-
{ (a, c) -> get(a) to other.get(c) },
64-
{ (b, d) -> { (a, c) -> set(b)(a) to other.set(d)(c) } }
70+
fun <C, D> split(other: Lens<C, D>): Lens<Tuple2<A, C>, Tuple2<B, D>> = Lens(
71+
{ (a, c) -> get(a) toT other.get(c) },
72+
{ (b, d) -> { (a, c) -> set(b)(a) toT other.set(d)(c) } }
6573
)
6674

6775
/**
6876
* Convenience method to create a pair of the target and a type C
6977
*/
70-
fun <C> first(): Lens<Pair<A, C>, Pair<B, C>> = Lens(
71-
{ (a, c) -> get(a) to c },
72-
{ (b, c) -> { (a, _) -> set(b)(a) to c } }
78+
fun <C> first(): Lens<Tuple2<A, C>, Tuple2<B, C>> = Lens(
79+
{ (a, c) -> get(a) toT c },
80+
{ (b, c) -> { (a, _) -> set(b)(a) toT c } }
7381
)
7482

7583
/**
7684
* Convenience method to create a pair of a type C and the target
7785
*/
78-
fun <C> second(): Lens<Pair<C, A>, Pair<C, B>> = Lens(
79-
{ (c, a) -> c to get(a) },
80-
{ (c, b) -> { (_, a) -> c to set(b)(a) } }
86+
fun <C> second(): Lens<Tuple2<C, A>, Tuple2<C, B>> = Lens(
87+
{ (c, a) -> c toT get(a) },
88+
{ (c, b) -> { (_, a) -> c toT set(b)(a) } }
8189
)
8290

8391
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package kategory.optics
2+
3+
import kategory.Applicative
4+
import kategory.Either
5+
import kategory.HK
6+
import kategory.Option
7+
import kategory.Tuple2
8+
import kategory.flatMap
9+
import kategory.identity
10+
import kategory.left
11+
import kategory.right
12+
import kategory.toT
13+
14+
/**
15+
* A [Prism] can be seen as a pair of functions: `reverseGet : B -> A` and `getOrModify: A -> Either<A,B>`
16+
*
17+
* - `reverseGet : B -> A` get the source type of a [Prism]
18+
* - `getOrModify: A -> Either<A,B>` get the target of a [Prism] or return the original value
19+
*
20+
* It encodes the relation between a Sum or CoProduct type (sealed class) and one of its element.
21+
*
22+
* @param A the source of a [Prism]
23+
* @param B the target of a [Prism]
24+
* @property getOrModify from an `B` we can produce an `A`
25+
* @property reverseGet get the target of a [Prism] or return the original value
26+
* @constructor Creates a Lens of type `A` with target `B`
27+
*/
28+
abstract class Prism<A, B> {
29+
30+
abstract fun getOrModify(a: A): Either<A, B>
31+
abstract fun reverseGet(b: B): A
32+
33+
companion object {
34+
operator fun <A,B> invoke(getOrModify: (A) -> Either<A, B>, reverseGet: (B) -> A) = object : Prism<A,B>() {
35+
override fun getOrModify(a: A): Either<A, B> = getOrModify(a)
36+
37+
override fun reverseGet(b: B): A = reverseGet(b)
38+
}
39+
}
40+
41+
/**
42+
* Get the target or nothing if `A` does not match the target
43+
*/
44+
fun getOption(a: A): Option<B> = getOrModify(a).toOption()
45+
46+
/**
47+
* Modify the target of a [Prism] with an Applicative function
48+
*/
49+
inline fun <reified F> modifyF(FA: Applicative<F> = kategory.applicative(), crossinline f: (B) -> HK<F, B>, a: A): HK<F, A> = getOrModify(a).fold(
50+
{ FA.pure(it) },
51+
{ FA.map(f(it), this::reverseGet) }
52+
)
53+
54+
/**
55+
* Modify the target of a [Prism] with a function
56+
*/
57+
inline fun modify(crossinline f: (B) -> B): (A) -> A = {
58+
getOrModify(it).fold(::identity, { reverseGet(f(it)) })
59+
}
60+
61+
/**
62+
* Modify the target of a [Prism] with a function
63+
*/
64+
inline fun modifyOption(crossinline f: (B) -> B): (A) -> Option<A> = { getOption(it).map { b -> reverseGet(f(b)) } }
65+
66+
/**
67+
* Set the target of a [Prism] with a value
68+
*/
69+
fun set(b: B): (A) -> A = modify { b }
70+
71+
infix fun <C> composePrism(other: Prism<B, C>): Prism<A, C> = Prism(
72+
{ a -> getOrModify(a).flatMap { b: B -> other.getOrModify(b).bimap({ set(it)(a) }, ::identity) } },
73+
{ reverseGet(other.reverseGet(it)) }
74+
)
75+
76+
/**
77+
* Set the target of a [Prism] with a value
78+
*/
79+
fun setOption(b: B): (A) -> Option<A> = modifyOption { b }
80+
81+
/**
82+
* Check if there is a target
83+
*/
84+
fun isNotEmpty(a: A): Boolean = getOption(a).isDefined
85+
86+
/**
87+
* Check if there is no target
88+
*/
89+
fun isEmpty(a: A): Boolean = !isNotEmpty(a)
90+
91+
/**
92+
* Find if the target satisfies the predicate
93+
*/
94+
inline fun find(crossinline p: (B) -> Boolean): (A) -> Option<B> = { getOption(it).flatMap { if (p(it)) Option.Some(it) else Option.None } }
95+
96+
/**
97+
* Check if there is a target and it satisfies the predicate
98+
*/
99+
inline fun exist(crossinline p: (B) -> Boolean): (A) -> Boolean = { getOption(it).fold({ false }, p) }
100+
101+
/**
102+
* Check if there is no target or the target satisfies the predicate
103+
*/
104+
inline fun all(crossinline p: (B) -> Boolean): (A) -> Boolean = { getOption(it).fold({ true }, p) }
105+
106+
/**
107+
* Convenience method to create a product of the target and a type C
108+
*/
109+
fun <C> first(): Prism<Tuple2<A, C>, Tuple2<B, C>> = Prism(
110+
{ (a, c) -> getOrModify(a).bimap({ it toT c }, { it toT c }) },
111+
{ (b, c) -> reverseGet(b) toT c }
112+
)
113+
114+
/**
115+
* Convenience method to create a product of a type C and the target
116+
*/
117+
fun <C> second(): Prism<Tuple2<C, A>, Tuple2<C, B>> = Prism(
118+
{ (c, a) -> getOrModify(a).bimap({ c toT it }, { c toT it }) },
119+
{ (c, b) -> c toT reverseGet(b) }
120+
)
121+
122+
}
123+
124+
/**
125+
* Convenience method to create a sum of the target and a type C
126+
*/
127+
fun <A, B, C> Prism<A, B>.left(): Prism<Either<A, C>, Either<B, C>> = Prism(
128+
{ it.fold({ a -> getOrModify(a).bimap({ it.left() }, { it.left() }) }, { c -> Either.Right(c.right()) }) },
129+
{
130+
when (it) {
131+
is Either.Left<B, C> -> Either.Left(reverseGet(it.a))
132+
is Either.Right<B, C> -> Either.Right(it.b)
133+
}
134+
}
135+
)
136+
137+
/**
138+
* Convenience method to create a sum of a type C and the target
139+
*/
140+
fun <A, B, C> Prism<A, B>.right(): Prism<Either<C, A>, Either<C, B>> = Prism(
141+
{ it.fold({ c -> Either.Right(c.left()) }, { a -> getOrModify(a).bimap({ it.right() }, { it.right() }) }) },
142+
{ it.map(this::reverseGet) }
143+
)

kategory-optics/src/test/kotlin/kategory/optics/LensLaws.kt

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import io.kotlintest.properties.forAll
55
import kategory.Applicative
66
import kategory.Eq
77
import kategory.Law
8-
import kategory.Lens
98
import kategory.compose
109
import kategory.exists
1110
import kategory.identity

kategory-optics/src/test/kotlin/kategory/optics/LensTest.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import io.kotlintest.KTestJUnitRunner
44
import io.kotlintest.properties.Gen
55
import io.kotlintest.properties.forAll
66
import kategory.Eq
7-
import kategory.Lens
87
import kategory.Option
8+
import kategory.Tuple2
99
import kategory.UnitSpec
1010
import kategory.applicative
1111
import kategory.genFunctionAToB
1212
import kategory.left
1313
import kategory.right
14+
import kategory.toT
1415
import org.junit.runner.RunWith
1516

1617
@RunWith(KTestJUnitRunner::class)
@@ -58,23 +59,23 @@ class LensTest : UnitSpec() {
5859
}
5960

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

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

7475
"Creating a second pair with a type should result in the value target" {
7576
val first = tokenLens.second<Int>()
7677
forAll(Gen.int(), TokenGen, { int: Int, token: Token ->
77-
first.get(int to token) == int to token.value
78+
first.get(int toT token) == int toT token.value
7879
})
7980
}
8081
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package kategory.optics
2+
3+
import io.kotlintest.properties.Gen
4+
import io.kotlintest.properties.forAll
5+
import kategory.Applicative
6+
import kategory.Eq
7+
import kategory.Law
8+
import kategory.compose
9+
import kategory.exists
10+
import kategory.identity
11+
12+
object PrismLaws {
13+
14+
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(
15+
Law("Prism law: partial round trip one way", { partialRoundTripOneWay(prism, aGen, EQA) }),
16+
Law("Prism law: round trip other way", { roundTripOtherWay(prism, bGen, EQB) }),
17+
Law("Prism law: modify identity", { modifyIdentity(prism, aGen, EQA) }),
18+
Law("Prism law: compose modify", { composeModify(prism, aGen, funcGen, EQA) }),
19+
Law("Prism law: consistent set modify", { consistentSetModify(prism, aGen, bGen, EQA) }),
20+
Law("Prism law: consistent get option modify id", { consistentGetOptionModifyId(prism, aGen, FA, EQB) })
21+
)
22+
23+
fun <A, B> partialRoundTripOneWay(prism: Prism<A, B>, aGen: Gen<A>, EQA: Eq<A>): Unit =
24+
forAll(aGen, { a ->
25+
EQA.eqv(prism.getOrModify(a).fold(::identity, prism::reverseGet), a)
26+
})
27+
28+
fun <A, B> roundTripOtherWay(prism: Prism<A, B>, bGen: Gen<B>, EQB: Eq<B>): Unit =
29+
forAll(bGen, { b ->
30+
prism.getOption(prism.reverseGet(b)).exists { EQB.eqv(it, b) }
31+
})
32+
33+
fun <A, B> modifyIdentity(prism: Prism<A, B>, aGen: Gen<A>, EQA: Eq<A>): Unit =
34+
forAll(aGen, { a ->
35+
EQA.eqv(prism.modify(::identity)(a), a)
36+
})
37+
38+
fun <A, B> composeModify(prism: Prism<A, B>, aGen: Gen<A>, funcGen: Gen<(B) -> B>, EQA: Eq<A>): Unit =
39+
forAll(aGen, funcGen, funcGen, { a, f, g ->
40+
EQA.eqv(prism.modify(g)(prism.modify(f)(a)), prism.modify(g compose f)(a))
41+
})
42+
43+
fun <A, B> consistentSetModify(prism: Prism<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>): Unit =
44+
forAll(aGen, bGen, { a, b ->
45+
EQA.eqv(prism.set(b)(a), prism.modify { b }(a))
46+
})
47+
48+
inline fun <A, B, reified F> consistentGetOptionModifyId(prism: Prism<A, B>, aGen: Gen<A>, FA: Applicative<F>, EQB: Eq<B>): Unit =
49+
forAll(aGen, { a ->
50+
prism.modifyF(FA, { FA.pure(it) }, a).exists { prism.getOption(it).exists { b -> prism.getOption(a).exists { EQB.eqv(b, it) } } }
51+
})
52+
53+
}

0 commit comments

Comments
 (0)