-
Notifications
You must be signed in to change notification settings - Fork 450
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
Improve Optics DSL with support for sum types and enable Each/At #803
Conversation
Codecov Report
@@ Coverage Diff @@
## master #803 +/- ##
============================================
+ Coverage 43.44% 43.57% +0.12%
- Complexity 582 584 +2
============================================
Files 282 284 +2
Lines 7211 7245 +34
Branches 812 809 -3
============================================
+ Hits 3133 3157 +24
- Misses 3791 3802 +11
+ Partials 287 286 -1
Continue to review full report at Codecov.
|
return metadata.asClassOrPackageDataWrapper(classElement) | ||
?: knownError("This annotation can't be used on this element") | ||
?: knownError("Arrow's annotation can't be used on $classElement") |
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.
can't you get the potential binary name for this element (class name) from the TypeElement
to include it in the error instead of the TypeElement instance reference? (ProcessingEnvironment.getElementUtils().getBinaryName(TypeElement)
).
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.
Actually for
@instance(String::class)
interface StringEqInstance : Eq<String> {
override fun String.eqv(b: String): Boolean = this == b
}
it prints e: error: Arrow's annotations can only be used on Kotlin classes. Not valid for java.lang.String
. Not sure if that's what you wanted?
toString()
is correctly implemented for Element
and it thus prints the class names.
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.
no, I was expecting to get the same but for the annotated class itself, like arrow.StringEqInstance
. Are you calling it for the classElement
argument?
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.
Yes, that makes sense. We need to improve error message.
I tried to write an Each
instance for String
and got This annotation can't be used on this element
. So I made a small change to improve what we currently have. Problem with a general error message like this is that we can't point the user in the right direction. So I would go as far as provide clear custom messages for each error case.
In this case something like.
e: error: @instance cannot be used for java.lang.String (or even better kotlin.String)
interface StringEqInstance : Eq<String>
^
Cannot generate instance method for arrow.data.instances.StringEqInstance.
|/** | ||
| * @receiver [${annotatedOptic.sourceClassName.removeBackticks()}] the instance you want to bind the dsl on. | ||
| * @return [$boundSetter] an intermediate optics that is bound to the instance. | ||
| */ | ||
|fun ${annotatedOptic.sourceClassName}.setter() = $boundSetter(this, arrow.optics.PSetter.id()) | ||
|""".trimMargin() | ||
|
||
fun processBoundSetter(sourceClassName: String, targetName: String, targetClassName: String, sourceName: String) = """ | ||
|inline val <T> $boundSetter<T, $sourceClassName>.$targetName: $boundSetter<T, $targetClassName> | ||
private fun processBoundSetter(sourceClassName: String, targetName: String, targetClassName: String, sourceName: String) = """ |
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.
maybe reorder to sourceName, sourceClassName, targetName, targetClassName
? The args feel quite disordered now
|
||
normalizedTargets.forEach { target -> | ||
|
||
element.normalizedTargets().forEach { target -> |
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.
nice cleaning
} | ||
|
||
private val Element.otherClassTypeErrorMessage |
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.
Not a blocker, but could be better to move these to a separated ErrorMessages.kt
or ErrorSyntax.kt
file, IMO. After all, these functions don't seem highly tied / related to the processor class itself.
@@ -74,7 +74,7 @@ fun <G, A, B, C> EitherOf<A, B>.traverse(f: (B) -> Kind<G, C>, GA: Applicative<G | |||
interface EitherTraverseInstance<L> : EitherFoldableInstance<L>, Traverse<EitherPartialOf<L>> { | |||
|
|||
override fun <G, A, B> Kind<EitherPartialOf<L>, A>.traverse(AP: Applicative<G>, f: (A) -> Kind<G, B>): Kind<G, Kind<EitherPartialOf<L>, B>> = | |||
fix().traverse(f, AP) | |||
traverse(f, AP) |
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.
is this due to typeclass methods defined as extfuns on top of kinds now?
@@ -99,7 +99,7 @@ interface TryFoldableInstance : Foldable<ForTry> { | |||
fix().foldRight(lb, f) | |||
} | |||
|
|||
fun <A, B, G> Try<A>.traverse(f: (A) -> Kind<G, B>, GA: Applicative<G>): Kind<G, Try<B>> = GA.run { | |||
fun <A, B, G> TryOf<A>.traverse(f: (A) -> Kind<G, B>, GA: Applicative<G>): Kind<G, Try<B>> = GA.run { |
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.
Ok, I guess it is.
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.
Actually, it's fixed in #793.
data class Company(val name: String, val address: Address) | ||
@optics | ||
data class Employee(val name: String, val company: Company?) | ||
@optics data class Street(val number: Int, val name: String) |
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.
Is it mandatory to annotate each one of the implementations of the sum type (sealed class) to generate the optics for each ? Shouldn't we be able to generate optics for all the implementations on the sealed hierarchy if you just annotate the parent sealed class itself? Just opening discussion, not a blocker.
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.
Yes, it is. Otherwise, it might get complex really quickly.
Given the network example, what is generated?
@optics sealed class NetworkResult
data class Success(val content: String): NetworkResult()
sealed class NetworkError: NetworkResult()
data class HttpError(val message: String): NetworkError()
object TimeOut: NetworkError()
Does it generate Prism<NetworkResult, Success>
, Prism<NetworkResult, NetworkError>
and everything applicable to Success
? Or does it also generate everything for HttpError
and Prisms for NetworkError
?
What happens if I pass arguments to @optics
? i.e. what happens if I change it to @optics([LENS])
? Does it fail because I cannot generate Lens
for NetworkResult? Or does it generate a Lens
for Success
and doesn't complain? What happens in same scenario when sealed class
only contains object
and thus cannot generate any Lens
?
We will create a ton of edge cases which I think will be extremely confusing for the users.
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.
Fair enough. I wasn't aware of the complexity around optics generation so it was probably a bit blind question.
We can rewrite this code with our generated dsl. | ||
|
||
```kotlin:ank | ||
networkResult.setter().networkError.httpError.message.modify(f) |
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.
why is it setter().networkError.httpError
and not setter().httpError
? How networkError
is required here if we just want to set the new modified message value when the type is HttpError
?
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.
That is because of the nested hierarchy of the sealed classes.
We need to compose Prism<NetworkResult, NetworkError>
with Prism<NetworkError, HttpError>
to get Prism<NeworkResult, HttpError>
.
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.
Yeah I've noticed now that there's a second level of inheritance there.
@@ -0,0 +1,76 @@ | |||
package arrow.optics |
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.
why is this docs helper file (or that's how it looks like) inside of the optics module and not the docs one?
@@ -47,7 +47,8 @@ interface ProcessorUtils : KotlinMetadataUtils { | |||
} | |||
|
|||
fun getClassOrPackageDataWrapper(classElement: TypeElement): ClassOrPackageDataWrapper { | |||
val metadata = classElement.kotlinMetadata ?: knownError("Arrow's annotations can only be used on Kotlin classes. Not valid for $classElement") | |||
val metadata = classElement.kotlinMetadata |
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.
what's the code style we're using for this repo?
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.
Not sure. I reformatted project with 2 spaces in my last commit and it moved this on two lines. Since I was unsure what the project style is for this case I left it so.
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.
ty!
@raulraja I added scoped syntax as discussed on Slack. https://github.com/arrow-kt/arrow/pull/803/files#diff-43ed86ff2970e9668758930fe702b695R81 The short version of discussion: |
Awesome cleanup 👏 |
Looks good @nomisRev :) |
…ow-kt#803) * Add support for Each and At in DSL * Rewrite Each and Traversal to work on concrete type instead of kind. * Improve error message * Generate DSL for sealed classes * Docs * Add scoped syntax for Each * Add scoped syntax for At
Quite a lot of stuff here :)
This will enable generation of DSL for sum types.
i.e. A (part of) JSON ADT
This also already shows the power of
every
which relies onEach
. The nameevery
comes from a discussion a while back for Helios, back then we still had lookups. Also in Helios we can hideEach
from the user. I am however unsure if we should still name itevery
overeach
since the use ofEach
is much more prominent now and I'd prefer not to rename theEach
typeclass. Thoughts?Use of
At
:Finally I also did an attempt to improve error messages in the code gen.
@pakoito I did not add any way to enable
each.run { data.list.every() }
since I thought it didn't belong in theEach
typeclass.