Skip to content

Commit 8c811ef

Browse files
authored
Merge pull request #251 from nomisRev/simon-optics-module
Optics module with initial lens impl
2 parents 543bfbd + 0f409b4 commit 8c811ef

File tree

7 files changed

+301
-1
lines changed

7 files changed

+301
-1
lines changed

kategory-optics/build.gradle

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
test {
2+
testLogging {
3+
events "passed", "skipped", "failed", "standardOut", "standardError"
4+
}
5+
}
6+
7+
dependencies {
8+
compile project(':kategory')
9+
compile project(':kategory-test')
10+
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
11+
testCompile "io.kotlintest:kotlintest:$kotlinTestVersion"
12+
testCompile project(':kategory-test')
13+
}
14+
15+
build.dependsOn ':detekt'
16+
17+
sourceCompatibility = javaVersion
18+
targetCompatibility = javaVersion
19+
20+
apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
21+
apply from: 'generated-kotlin-sources.gradle'
22+
apply plugin: 'kotlin-kapt'
23+
24+
compileKotlin.kotlinOptions.freeCompilerArgs += ["-Xskip-runtime-version-check"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apply plugin: 'idea'
2+
3+
idea {
4+
module {
5+
sourceDirs += files(
6+
'build/generated/source/kapt/main',
7+
'build/generated/source/kaptKotlin/main',
8+
'build/tmp/kapt/main/kotlinGenerated')
9+
generatedSourceDirs += files(
10+
'build/generated/source/kapt/main',
11+
'build/generated/source/kaptKotlin/main',
12+
'build/tmp/kapt/main/kotlinGenerated')
13+
}
14+
}

kategory-optics/gradle.properties

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Maven publishing configuration
2+
POM_NAME=Kategory-Optics
3+
POM_ARTIFACT_ID=kategory-optics
4+
POM_PACKAGING=jar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package kategory
2+
3+
/**
4+
* A [Lens] can be seen as a pair of functions `get: (A) -> B` and `set: (B) -> (A) -> A`
5+
* - `get: (A) -> B` i.e. from an `A`, we can extract an `B`
6+
* - `set: (B) -> (A) -> A` i.e. if we replace target value by `B` in an `A`, we obtain another modified `A`
7+
*
8+
* @param A the source of a [Lens]
9+
* @param B the target of a [Lens]
10+
* @property get from an `A` we can extract a `B`
11+
* @property set replace the target value by `B` in an `A` so we obtain another modified `A`
12+
* @constructor Creates a Lens of type `A` with target `B`.
13+
*/
14+
abstract class Lens<A, B> {
15+
16+
abstract fun get(a: A): B
17+
abstract fun set(b: B): (A) -> A
18+
19+
companion object {
20+
operator fun <A, B> invoke(get: (A) -> B, set: (B) -> (A) -> A) = object : Lens<A, B>() {
21+
override fun get(a: A): B = get(a)
22+
23+
override fun set(b: B): (A) -> A = set(b)
24+
}
25+
}
26+
27+
/**
28+
* Modify the target of a [Lens] using a function `(B) -> B`
29+
*/
30+
inline fun modify(f: (B) -> B, a: A): A = set(f(get(a)))(a)
31+
32+
/**
33+
* Modify the target of a [Lens] using Functor function
34+
*/
35+
inline fun <reified F> modifyF(FF: Functor<F> = functor(), f: (B) -> HK<F, B>, a: A): HK<F, A> =
36+
FF.map(f(get(a)), { set(it)(a) })
37+
38+
/**
39+
* Find if the target satisfies the predicate
40+
*/
41+
inline fun find(crossinline p: (B) -> Boolean): (A) -> Option<B> = {
42+
val a = get(it)
43+
if (p(a)) Option.Some(a) else Option.None
44+
}
45+
46+
/**
47+
* Checks if the target of a [Lens] satisfies the predicate
48+
*/
49+
inline fun exist(crossinline p: (B) -> Boolean): (A) -> Boolean = { p(get(it)) }
50+
51+
/**
52+
* Join two [Lens] with the same target
53+
*/
54+
fun <C> choice(other: Lens<C, B>): Lens<Either<A, C>, B> = Lens(
55+
{ it.fold(this::get, other::get) },
56+
{ b -> { it.bimap(set(b), other.set(b)) } }
57+
)
58+
59+
/**
60+
* Pair two disjoint [Lens]
61+
*/
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) } }
65+
)
66+
67+
/**
68+
* Convenience method to create a pair of the target and a type C
69+
*/
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 } }
73+
)
74+
75+
/**
76+
* Convenience method to create a pair of a type C and the target
77+
*/
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) } }
81+
)
82+
83+
/**
84+
* Compose a [Lens] with another [Lens]
85+
*/
86+
infix fun <C> composeLens(l: Lens<B, C>): Lens<A, C> = Lens(
87+
{ a -> l.get(get(a)) },
88+
{ c -> { a -> set(l.set(c)(get(a)))(a) } }
89+
)
90+
91+
/**
92+
* plus operator overload to compose lenses
93+
*/
94+
operator fun <C> plus(other: Lens<B, C>): Lens<A, C> = composeLens(other)
95+
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.Lens
9+
import kategory.compose
10+
import kategory.exists
11+
import kategory.identity
12+
13+
object LensLaws {
14+
15+
inline fun <A, B, reified F> laws(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, funcGen: Gen<(B) -> B>, EQA: Eq<A>, EQB: Eq<B>, FA: Applicative<F>) = listOf(
16+
Law("Lens law: get set", { lensGetSet(lens, aGen, EQA) }),
17+
Law("Lens law: set get", { lensSetGet(lens, aGen, bGen, EQB) }),
18+
Law("Lens law: is set idempotent", { lensSetIdempotent(lens, aGen, bGen, EQA) }),
19+
Law("Lens law: modify identity", { lensModifyIdentity(lens, aGen, bGen, EQA) }),
20+
Law("Lens law: compose modify", { lensComposeModify(lens, aGen, funcGen, EQA) }),
21+
Law("Lens law: consistent set modify", { lensConsistentSetModify(lens, aGen, bGen, EQA) }),
22+
Law("Lens law: consistent modify modify id", { lensConsistentModifyModifyId(lens, aGen, funcGen, EQA, FA) }),
23+
Law("Lens law: consistent get modify id", { lensConsistentGetModifyid(lens, aGen, EQB, FA) })
24+
)
25+
26+
fun <A, B> lensGetSet(lens: Lens<A, B>, aGen: Gen<A>, EQA: Eq<A>) =
27+
forAll(aGen, { a ->
28+
EQA.eqv(lens.set(lens.get(a))(a), a)
29+
})
30+
31+
fun <A, B> lensSetGet(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQB: Eq<B>) =
32+
forAll(aGen, bGen, { a, b ->
33+
EQB.eqv(lens.get(lens.set(b)(a)), b)
34+
})
35+
36+
fun <A, B> lensSetIdempotent(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>) =
37+
forAll(aGen, bGen, { a, b ->
38+
EQA.eqv(lens.set(b)(lens.set(b)(a)), lens.set(b)(a))
39+
})
40+
41+
fun <A, B> lensModifyIdentity(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>) =
42+
forAll(aGen, bGen, { a, b ->
43+
EQA.eqv(lens.modify(::identity, a), a)
44+
})
45+
46+
fun <A, B> lensComposeModify(lens: Lens<A, B>, aGen: Gen<A>, funcGen: Gen<(B) -> B>, EQA: Eq<A>) =
47+
forAll(aGen, funcGen, funcGen, { a, f, g ->
48+
EQA.eqv(lens.modify(g, lens.modify(f, a)), lens.modify(g compose f, a))
49+
})
50+
51+
fun <A, B> lensConsistentSetModify(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>) =
52+
forAll(aGen, bGen, { a, b ->
53+
EQA.eqv(lens.set(b)(a), lens.modify({ b }, a))
54+
})
55+
56+
inline fun <A, B, reified F> lensConsistentModifyModifyId(lens: Lens<A, B>, aGen: Gen<A>, funcGen: Gen<(B) -> B>, EQA: Eq<A>, FA: Applicative<F>) =
57+
forAll(aGen, funcGen, { a, f ->
58+
lens.modifyF(FA, { FA.pure(f(it)) }, a).exists { EQA.eqv(lens.modify(f, a), it) }
59+
})
60+
61+
inline fun <A, B, reified F> lensConsistentGetModifyid(lens: Lens<A, B>, aGen: Gen<A>, EQB: Eq<B>, FA: Applicative<F>) =
62+
forAll(aGen, { a ->
63+
lens.modifyF(FA, { FA.pure(it) }, a).exists { EQB.eqv(lens.get(a), lens.get(it)) }
64+
})
65+
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package kategory.optics
2+
3+
import io.kotlintest.KTestJUnitRunner
4+
import io.kotlintest.properties.Gen
5+
import io.kotlintest.properties.forAll
6+
import kategory.Eq
7+
import kategory.Lens
8+
import kategory.Option
9+
import kategory.UnitSpec
10+
import kategory.applicative
11+
import kategory.genFunctionAToB
12+
import kategory.left
13+
import kategory.right
14+
import org.junit.runner.RunWith
15+
16+
@RunWith(KTestJUnitRunner::class)
17+
class LensTest : UnitSpec() {
18+
19+
private val tokenLens: Lens<Token, String> = Lens(
20+
{ token: Token -> token.value },
21+
{ value: String -> { token: Token -> token.copy(value = value) } }
22+
)
23+
24+
init {
25+
testLaws(
26+
LensLaws.laws(
27+
lens = tokenLens,
28+
aGen = TokenGen,
29+
bGen = Gen.string(),
30+
funcGen = genFunctionAToB(Gen.string()),
31+
EQA = Eq.any(),
32+
EQB = Eq.any(),
33+
FA = Option.applicative()
34+
)
35+
)
36+
37+
"Finding a target using a predicate within a Lens should be wrapped in the correct option result" {
38+
forAll({ predicate: Boolean ->
39+
tokenLens.find { predicate }(Token("any value")).isDefined == predicate
40+
})
41+
}
42+
43+
"Checking existence predicate over the target should result in same result as predicate" {
44+
forAll({ predicate: Boolean ->
45+
tokenLens.exist { predicate }(Token("any value")) == predicate
46+
})
47+
}
48+
49+
"Joining two lenses together with same target should yield same result" {
50+
val userTokenStringLens = userLens composeLens tokenLens
51+
val joinedLens = tokenLens.choice(userTokenStringLens)
52+
53+
forAll({ tokenValue: String ->
54+
val token = Token(tokenValue)
55+
val user = User(token)
56+
joinedLens.get(token.left()) == joinedLens.get(user.right())
57+
})
58+
}
59+
60+
"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+
forAll(TokenGen, UserGen, { token: Token, user: User ->
63+
spiltLens.get(token to user) == token.value to user.token
64+
})
65+
}
66+
67+
"Creating a first pair with a type should result in the target to value" {
68+
val first = tokenLens.first<Int>()
69+
forAll(TokenGen, Gen.int(), { token: Token, int: Int ->
70+
first.get(token to int) == token.value to int
71+
})
72+
}
73+
74+
"Creating a second pair with a type should result in the value target" {
75+
val first = tokenLens.second<Int>()
76+
forAll(Gen.int(), TokenGen, { int: Int, token: Token ->
77+
first.get(int to token) == int to token.value
78+
})
79+
}
80+
}
81+
82+
private data class Token(val value: String)
83+
private object TokenGen : Gen<Token> {
84+
override fun generate() = Token(Gen.string().generate())
85+
}
86+
87+
private data class User(val token: Token)
88+
private object UserGen : Gen<User> {
89+
override fun generate() = User(TokenGen.generate())
90+
}
91+
92+
private val userLens: Lens<User, Token> = Lens(
93+
{ user: User -> user.token },
94+
{ token: Token -> { user: User -> user.copy(token = token) } }
95+
)
96+
}

settings.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414
* limitations under the License.
1515
*/
1616

17-
include ':kategory', ':kategory-test', ':kategory-effects', ':kategory-effects-test', ':kategory-recursion', ':kategory-docs'
17+
include ':kategory', ':kategory-test', ':kategory-effects', ':kategory-effects-test', ':kategory-recursion', ':kategory-docs', 'kategory-optics'
1818
rootProject.name = 'kategory-parent'

0 commit comments

Comments
 (0)