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

Optics module with initial lens impl #251

Merged
merged 4 commits into from
Sep 5, 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
24 changes: 24 additions & 0 deletions kategory-optics/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
test {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}

dependencies {
compile project(':kategory')
compile project(':kategory-test')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
testCompile "io.kotlintest:kotlintest:$kotlinTestVersion"
testCompile project(':kategory-test')
}

build.dependsOn ':detekt'

sourceCompatibility = javaVersion
targetCompatibility = javaVersion

apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
apply from: 'generated-kotlin-sources.gradle'
apply plugin: 'kotlin-kapt'

compileKotlin.kotlinOptions.freeCompilerArgs += ["-Xskip-runtime-version-check"]
14 changes: 14 additions & 0 deletions kategory-optics/generated-kotlin-sources.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apply plugin: 'idea'

idea {
module {
sourceDirs += files(
'build/generated/source/kapt/main',
'build/generated/source/kaptKotlin/main',
'build/tmp/kapt/main/kotlinGenerated')
generatedSourceDirs += files(
'build/generated/source/kapt/main',
'build/generated/source/kaptKotlin/main',
'build/tmp/kapt/main/kotlinGenerated')
}
}
4 changes: 4 additions & 0 deletions kategory-optics/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Maven publishing configuration
POM_NAME=Kategory-Optics
POM_ARTIFACT_ID=kategory-optics
POM_PACKAGING=jar
96 changes: 96 additions & 0 deletions kategory-optics/src/main/kotlin/kategory/optics/Lens.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package kategory

/**
* A [Lens] can be seen as a pair of functions `get: (A) -> B` and `set: (B) -> (A) -> A`
* - `get: (A) -> B` i.e. from an `A`, we can extract an `B`
* - `set: (B) -> (A) -> A` i.e. if we replace target value by `B` in an `A`, we obtain another modified `A`
*
* @param A the source of a [Lens]
* @param B the target of a [Lens]
* @property get from an `A` we can extract a `B`
* @property set replace the target value by `B` in an `A` so we obtain another modified `A`
* @constructor Creates a Lens of type `A` with target `B`.
*/
abstract class Lens<A, B> {

abstract fun get(a: A): B
abstract fun set(b: B): (A) -> A

companion object {
operator fun <A, B> invoke(get: (A) -> B, set: (B) -> (A) -> A) = object : Lens<A, B>() {
override fun get(a: A): B = get(a)

override fun set(b: B): (A) -> A = set(b)
}
}

/**
* Modify the target of a [Lens] using a function `(B) -> B`
*/
inline fun modify(f: (B) -> B, a: A): A = set(f(get(a)))(a)

/**
* Modify the target of a [Lens] using Functor function
*/
inline fun <reified F> modifyF(FF: Functor<F> = functor(), f: (B) -> HK<F, B>, a: A): HK<F, A> =
FF.map(f(get(a)), { set(it)(a) })

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

/**
* Checks if the target of a [Lens] satisfies the predicate
*/
inline fun exist(crossinline p: (B) -> Boolean): (A) -> Boolean = { p(get(it)) }

/**
* Join two [Lens] with the same target
*/
fun <C> choice(other: Lens<C, B>): Lens<Either<A, C>, B> = Lens(
{ it.fold(this::get, other::get) },
{ b -> { it.bimap(set(b), other.set(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) } }
)

/**
* 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 } }
)

/**
* 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) } }
)

/**
* Compose a [Lens] with another [Lens]
*/
infix fun <C> composeLens(l: Lens<B, C>): Lens<A, C> = Lens(
{ a -> l.get(get(a)) },
{ c -> { a -> set(l.set(c)(get(a)))(a) } }
)

/**
* plus operator overload to compose lenses
*/
operator fun <C> plus(other: Lens<B, C>): Lens<A, C> = composeLens(other)

}
66 changes: 66 additions & 0 deletions kategory-optics/src/test/kotlin/kategory/optics/LensLaws.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package kategory.optics

import io.kotlintest.properties.Gen
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

object LensLaws {

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(
Law("Lens law: get set", { lensGetSet(lens, aGen, EQA) }),
Law("Lens law: set get", { lensSetGet(lens, aGen, bGen, EQB) }),
Law("Lens law: is set idempotent", { lensSetIdempotent(lens, aGen, bGen, EQA) }),
Law("Lens law: modify identity", { lensModifyIdentity(lens, aGen, bGen, EQA) }),
Law("Lens law: compose modify", { lensComposeModify(lens, aGen, funcGen, EQA) }),
Law("Lens law: consistent set modify", { lensConsistentSetModify(lens, aGen, bGen, EQA) }),
Law("Lens law: consistent modify modify id", { lensConsistentModifyModifyId(lens, aGen, funcGen, EQA, FA) }),
Law("Lens law: consistent get modify id", { lensConsistentGetModifyid(lens, aGen, EQB, FA) })
)

fun <A, B> lensGetSet(lens: Lens<A, B>, aGen: Gen<A>, EQA: Eq<A>) =
forAll(aGen, { a ->
EQA.eqv(lens.set(lens.get(a))(a), a)
})

fun <A, B> lensSetGet(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQB: Eq<B>) =
forAll(aGen, bGen, { a, b ->
EQB.eqv(lens.get(lens.set(b)(a)), b)
})

fun <A, B> lensSetIdempotent(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>) =
forAll(aGen, bGen, { a, b ->
EQA.eqv(lens.set(b)(lens.set(b)(a)), lens.set(b)(a))
})

fun <A, B> lensModifyIdentity(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>) =
forAll(aGen, bGen, { a, b ->
EQA.eqv(lens.modify(::identity, a), a)
})

fun <A, B> lensComposeModify(lens: Lens<A, B>, aGen: Gen<A>, funcGen: Gen<(B) -> B>, EQA: Eq<A>) =
forAll(aGen, funcGen, funcGen, { a, f, g ->
EQA.eqv(lens.modify(g, lens.modify(f, a)), lens.modify(g compose f, a))
})

fun <A, B> lensConsistentSetModify(lens: Lens<A, B>, aGen: Gen<A>, bGen: Gen<B>, EQA: Eq<A>) =
forAll(aGen, bGen, { a, b ->
EQA.eqv(lens.set(b)(a), lens.modify({ b }, a))
})

inline fun <A, B, reified F> lensConsistentModifyModifyId(lens: Lens<A, B>, aGen: Gen<A>, funcGen: Gen<(B) -> B>, EQA: Eq<A>, FA: Applicative<F>) =
forAll(aGen, funcGen, { a, f ->
lens.modifyF(FA, { FA.pure(f(it)) }, a).exists { EQA.eqv(lens.modify(f, a), it) }
})

inline fun <A, B, reified F> lensConsistentGetModifyid(lens: Lens<A, B>, aGen: Gen<A>, EQB: Eq<B>, FA: Applicative<F>) =
forAll(aGen, { a ->
lens.modifyF(FA, { FA.pure(it) }, a).exists { EQB.eqv(lens.get(a), lens.get(it)) }
})

}
96 changes: 96 additions & 0 deletions kategory-optics/src/test/kotlin/kategory/optics/LensTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package kategory.optics

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.UnitSpec
import kategory.applicative
import kategory.genFunctionAToB
import kategory.left
import kategory.right
import org.junit.runner.RunWith

@RunWith(KTestJUnitRunner::class)
class LensTest : UnitSpec() {

private val tokenLens: Lens<Token, String> = Lens(
{ token: Token -> token.value },
{ value: String -> { token: Token -> token.copy(value = value) } }
)

init {
testLaws(
LensLaws.laws(
lens = tokenLens,
aGen = TokenGen,
bGen = Gen.string(),
funcGen = genFunctionAToB(Gen.string()),
EQA = Eq.any(),
EQB = Eq.any(),
FA = Option.applicative()
)
)

"Finding a target using a predicate within a Lens should be wrapped in the correct option result" {
forAll({ predicate: Boolean ->
tokenLens.find { predicate }(Token("any value")).isDefined == predicate
})
}

"Checking existence predicate over the target should result in same result as predicate" {
forAll({ predicate: Boolean ->
tokenLens.exist { predicate }(Token("any value")) == predicate
})
}

"Joining two lenses together with same target should yield same result" {
val userTokenStringLens = userLens composeLens tokenLens
val joinedLens = tokenLens.choice(userTokenStringLens)

forAll({ tokenValue: String ->
val token = Token(tokenValue)
val user = User(token)
joinedLens.get(token.left()) == joinedLens.get(user.right())
})
}

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

"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
})
}
}

private data class Token(val value: String)
private object TokenGen : Gen<Token> {
override fun generate() = Token(Gen.string().generate())
}

private data class User(val token: Token)
private object UserGen : Gen<User> {
override fun generate() = User(TokenGen.generate())
}

private val userLens: Lens<User, Token> = Lens(
{ user: User -> user.token },
{ token: Token -> { user: User -> user.copy(token = token) } }
)
}
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
* limitations under the License.
*/

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