-
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
Optics based on kotlin.reflect #2612
Merged
Merged
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ca3283f
Optics based on kotlin.reflect
serras 0014fe3
Move to a new package
serras 1923589
Update API files
serras d89fa09
Remove unneeded dependency
serras a8728be
Docs
serras 9a8fab7
Merge branch 'main' into optics-reflection
serras 820743f
Merge branch 'main' into optics-reflection
serras 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
plugins { | ||
id(libs.plugins.kotlin.multiplatform.get().pluginId) | ||
alias(libs.plugins.arrowGradleConfig.kotlin) | ||
alias(libs.plugins.arrowGradleConfig.publish) | ||
} | ||
|
||
apply(plugin = "io.kotest.multiplatform") | ||
|
||
apply(from = property("TEST_COVERAGE")) | ||
apply(from = property("ANIMALSNIFFER_MPP")) | ||
|
||
kotlin { | ||
sourceSets { | ||
jvmMain { | ||
dependencies { | ||
api(projects.arrowCore) | ||
api(projects.arrowOptics) | ||
api(libs.kotlin.stdlibCommon) | ||
implementation(libs.kotlin.stdlibJDK8) | ||
api(libs.kotlin.reflect) | ||
} | ||
} | ||
jvmTest { | ||
dependencies { | ||
implementation(projects.arrowOpticsTest) | ||
implementation(libs.kotlin.stdlibJDK8) | ||
implementation(libs.junitJupiterEngine) | ||
implementation(libs.kotlin.reflect) | ||
} | ||
} | ||
} | ||
} |
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,4 @@ | ||
# Maven publishing configuration | ||
pom.name=Arrow Optics for Kotlin Reflection | ||
# Build configuration | ||
kapt.incremental.apt=false |
64 changes: 64 additions & 0 deletions
64
arrow-libs/optics/arrow-optics-reflect/src/jvmMain/kotlin/arrow/optics/Reflection.kt
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,64 @@ | ||
package arrow.optics | ||
|
||
import arrow.core.Either | ||
import arrow.core.left | ||
import arrow.core.right | ||
import kotlin.reflect.* | ||
import kotlin.reflect.full.instanceParameter | ||
import kotlin.reflect.full.memberFunctions | ||
|
||
/** Focuses on those elements of the specified [klass] */ | ||
public fun <S: Any, A: S> instance(klass: KClass<A>): Prism<S, A> = | ||
object: Prism<S, A> { | ||
override fun getOrModify(source: S): Either<S, A> = | ||
klass.safeCast(source)?.right() ?: source.left() | ||
override fun reverseGet(focus: A): S = focus | ||
} | ||
|
||
/** Focuses on those elements of the specified class */ | ||
public inline fun <S: Any, reified A: S> instance(): Prism<S, A> = | ||
object: Prism<S, A> { | ||
override fun getOrModify(source: S): Either<S, A> = | ||
(source as? A)?.right() ?: source.left() | ||
override fun reverseGet(focus: A): S = focus | ||
} | ||
|
||
/** Focuses on a given field */ | ||
public val <S, A> ((S) -> A).ogetter: Getter<S, A> | ||
get() = Getter { s -> this(s) } | ||
|
||
/** | ||
* [Lens] that focuses on a field in a data class | ||
* | ||
* WARNING: this should only be called on data classes, | ||
* but that is checked only at runtime! | ||
*/ | ||
public val <S, A> KProperty1<S, A>.lens: Lens<S, A> | ||
get() = PLens( | ||
get = this, | ||
set = { s, a -> clone(this, s, a) } | ||
) | ||
|
||
/** [Optional] that focuses on a nullable field */ | ||
public val <S, A> KProperty1<S, A?>.optional: Optional<S, A> | ||
get() = lens compose Optional.nullable() | ||
|
||
public val <S, A> ((S) -> Iterable<A>).iter: Fold<S, A> | ||
get() = ogetter compose Fold.iterable() | ||
|
||
public val <S, A> KProperty1<S, List<A>>.every: Every<S, A> | ||
get() = lens compose Every.list() | ||
|
||
public val <S, K, A> KProperty1<S, Map<K, A>>.values: Every<S, A> | ||
get() = lens compose Every.map() | ||
|
||
private fun <S, A> clone(prop: KProperty1<S, A>, value: S, newField: A): S { | ||
// based on https://stackoverflow.com/questions/49511098/call-data-class-copy-via-reflection | ||
val klass = prop.instanceParameter?.type?.classifier as? KClass<*> | ||
val copy = klass?.memberFunctions?.firstOrNull { it.name == "copy" } | ||
if (klass == null || !klass.isData || copy == null) { | ||
throw IllegalArgumentException("may only be used with data classes") | ||
} | ||
val fieldParam = copy.parameters.first { it.name == prop.name } | ||
return copy.callBy(mapOf(copy.instanceParameter!! to value, fieldParam to newField)) as S | ||
} |
55 changes: 55 additions & 0 deletions
55
arrow-libs/optics/arrow-optics-reflect/src/jvmTest/kotlin/arrow/optics/ReflectionTest.kt
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,55 @@ | ||
package arrow.optics | ||
|
||
import arrow.core.test.UnitSpec | ||
import io.kotest.matchers.shouldBe | ||
import io.kotest.property.Arb | ||
import io.kotest.property.arbitrary.int | ||
import io.kotest.property.arbitrary.string | ||
|
||
data class Person(val name: String, val friends: List<String>) | ||
|
||
sealed interface Cutlery | ||
object Fork: Cutlery | ||
object Spoon: Cutlery | ||
|
||
object ReflectionTest: UnitSpec() { | ||
init { | ||
"optional for function" { | ||
checkAll(Arb.list(Arb.int())) { ints -> | ||
val firsty = { it: List<Int> -> it.firstOrNull() } | ||
firsty.ogetter.get(ints) shouldBe ints.firstOrNull() | ||
} | ||
} | ||
|
||
"lenses for field, get" { | ||
checkAll(Arb.string(), Arb.list(Arb.string())) { nm, fs -> | ||
val p = Person(nm, fs.toMutableList()) | ||
Person::name.lens.get(p) shouldBe nm | ||
} | ||
} | ||
|
||
"lenses for field, set" { | ||
checkAll(Arb.string(), Arb.list(Arb.string())) { nm, fs -> | ||
val p = Person(nm, fs.toMutableList()) | ||
val m = Person::name.lens.modify(p) { it.capitalize() } | ||
m shouldBe Person(nm.capitalize(), fs) | ||
} | ||
} | ||
|
||
"traversal for list, set" { | ||
checkAll(Arb.string(), Arb.list(Arb.string())) { nm, fs -> | ||
val p = Person(nm, fs) | ||
val m = Person::friends.every.modify(p) { it.capitalize() } | ||
m shouldBe Person(nm, fs.map { it.capitalize() }) | ||
} | ||
} | ||
|
||
"instances" { | ||
val things = listOf(Fork, Spoon, Fork) | ||
val forks = Every.list<Cutlery>() compose instance<Cutlery, Fork>() | ||
val spoons = Every.list<Cutlery>() compose instance<Cutlery, Fork>() | ||
forks.size(things) shouldBe 2 | ||
spoons.size(things) shouldBe 1 | ||
} | ||
} | ||
} |
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
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
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.
So this exposes the API of Kotlin Std, and uses the JDK8 implementation?
I'm still not a 100% sure how this works, and what is the correct config :/
I don't we're doing different things in different modules.. but off-topic for this PR.
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'm not sure 100% either. In any case, I've removed the
api
dependency onstdlibCommon
and just left theimplementation
for the JDK version.