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

Improve Optics DSL with support for sum types and enable Each/At #803

Merged
merged 19 commits into from
Apr 19, 2018

Conversation

nomisRev
Copy link
Member

@nomisRev nomisRev commented Apr 17, 2018

Quite a lot of stuff here :)

This will enable generation of DSL for sum types.

i.e. A (part of) JSON ADT

@optics sealed class Json 
@optics data class JsString(val string: string): Json()
@optics sealed class JsNumber : Json()  
@optics data class JsInt(val int: Int) : Json() 
@optics data class JsDouble(val double: Double) : Json() 
@optics data class JsArray(val value: List<Json>) : Json()

val jsArrayEach: Each<JsArray, Json> = object : Each<JsArray, Json> {
  override fun each(): Traversal<JsArray, Json> = jsArrayIso() compose ListTraversal()
}

val json: Json = JsArray(listOf(
  JsInt(1),
  JsString("Hello"),
  JsInt(2),
  JsInt(3),
  JsDouble(4.0)
))

json.setter().jsArray.every(jsArrayEach).jsInt.int.modify { it + 1 }
//JsArray(value=[JsInt(int=2), JsString(value=Hello), JsInt(int=3), JsInt(int=4), JsDouble(double=4.0)])

This also already shows the power of every which relies on Each. The name every comes from a discussion a while back for Helios, back then we still had lookups. Also in Helios we can hide Each from the user. I am however unsure if we should still name it every over each since the use of Each is much more prominent now and I'd prefer not to rename the Each typeclass. Thoughts?

Use of At:

@optics data class Db(val content: MapK<Keys, String>)

sealed class Keys
object One : Keys()
object Two : Keys()
object Three : Keys()
object Four : Keys()

val db = Db(mapOf(
  One to "one",
  Two to "two",
  Three to "three",
  Four to "four"
).k())

db.setter().content.at(MapK.at(), One).some.modify(String::reversed)
//Db(content=MapK(map={arrow.optics.One@a09ee92=eno, arrow.optics.Two@30f39991=two, arrow.optics.Three@452b3a41=three, arrow.optics.Four@4a574795=four}))

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 the Eachtypeclass.

@nomisRev nomisRev changed the title [WIPImprove Optics DSL with support for sum types and enable Each/At [WIP] Improve Optics DSL with support for sum types and enable Each/At Apr 17, 2018
@nomisRev nomisRev changed the title [WIP] Improve Optics DSL with support for sum types and enable Each/At Improve Optics DSL with support for sum types and enable Each/At Apr 17, 2018
@codecov
Copy link

codecov bot commented Apr 17, 2018

Codecov Report

Merging #803 into master will increase coverage by 0.12%.
The diff coverage is 58.9%.

Impacted file tree graph

@@             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
Impacted Files Coverage Δ Complexity Δ
...tics/src/main/kotlin/arrow/optics/instances/set.kt 100% <ø> (ø) 0 <0> (?)
...ics/src/main/kotlin/arrow/optics/typeclasses/At.kt 0% <0%> (ø) 0 <0> (ø) ⬇️
...src/main/java/arrow/common/utils/ProcessorUtils.kt 18.07% <0%> (-0.23%) 0 <0> (ø)
...rc/main/kotlin/arrow/optics/instances/sequencek.kt 45.45% <0%> (-25.98%) 0 <0> (ø)
...src/main/java/arrow/optics/BoundSetterGenerator.kt 20% <0%> (ø) 2 <0> (ø) ⬇️
...tics/src/main/kotlin/arrow/optics/syntax/option.kt 0% <0%> (ø) 0 <0> (?)
...s/src/main/kotlin/arrow/optics/typeclasses/Each.kt 0% <0%> (ø) 0 <0> (ø) ⬇️
...tics/src/main/kotlin/arrow/optics/instances/try.kt 100% <100%> (ø) 0 <0> (ø) ⬇️
...s/src/main/kotlin/arrow/optics/instances/option.kt 100% <100%> (ø) 0 <0> (ø) ⬇️
...ics/src/main/kotlin/arrow/optics/instances/list.kt 90.47% <100%> (ø) 0 <0> (?)
... and 14 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0ce64f7...ac707b1. Read the comment docs.

return metadata.asClassOrPackageDataWrapper(classElement)
?: knownError("This annotation can't be used on this element")
?: knownError("Arrow's annotation can't be used on $classElement")
Copy link
Member

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)).

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

@nomisRev nomisRev Apr 18, 2018

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) = """
Copy link
Member

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 ->
Copy link
Member

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
Copy link
Member

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.ktor 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)
Copy link
Member

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 {
Copy link
Member

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.

Copy link
Member Author

@nomisRev nomisRev Apr 18, 2018

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)
Copy link
Member

@JorgeCastilloPrz JorgeCastilloPrz Apr 17, 2018

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.

Copy link
Member Author

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.

Copy link
Member

@JorgeCastilloPrz JorgeCastilloPrz Apr 18, 2018

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)
Copy link
Member

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 ?

Copy link
Member Author

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>.

Copy link
Member

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
Copy link
Member

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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

@JorgeCastilloPrz JorgeCastilloPrz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty!

@nomisRev
Copy link
Member Author

nomisRev commented Apr 18, 2018

@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:
I initially didn't add any scoped syntax for Each and At because it creates a dependency on BoundSetter. Which can be argued that it might not belong in a typeclass. However, we decided that the benefit outweighs the possible downside.

@raulraja
Copy link
Member

Awesome cleanup 👏

@JorgeCastilloPrz
Copy link
Member

Looks good @nomisRev :)

@nomisRev nomisRev merged commit 1c1114a into master Apr 19, 2018
@nomisRev nomisRev deleted the simon-optics-dsl branch April 19, 2018 06:57
RawToast pushed a commit to RawToast/kategory that referenced this pull request Jul 18, 2018
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants