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

Add support for Debug derivation #1076

Open
wants to merge 2 commits into
base: series/2.x
Choose a base branch
from

Conversation

alphaho
Copy link
Collaborator

@alphaho alphaho commented Feb 11, 2023

Debug is a nice feature, but without the auto derivation, it's not very practical to use it in a project.

This PR adds the support for auto derivation through magnolia. But due to the limitation of magnolia, it requires the type parameter in Debug to be invariant. Which is a drawback of adding the support.

@adamgfraser
Copy link
Contributor

Derivation of instances of functional abstractions for sum and product types is great but we should not be compromising our representation of the underlying nature of these abstractions to accommodate implementation limitations of particular third party libraries. Scala 3 now supports derivation for contravariant types directly and we can pursue other solutions such as zio-deriving for Scala 2 if we feel that is worthwhile.

@alphaho
Copy link
Collaborator Author

alphaho commented Feb 11, 2023

That makes sense. Let me checkout zio-deriving and see whether I can overcome that limitation.

@alphaho alphaho marked this pull request as draft February 11, 2023 08:33
Also make Debug to be invariant or it can't derive the typeclass for sealed traits
@alphaho
Copy link
Collaborator Author

alphaho commented Feb 11, 2023

@adamgfraser I've tried zio-deriving, and run into the same issue.
It would get StackOverflow in the auto derived instance for a sealed trait. It turns out that is a problem with the implicit resolution with the contravariant type. It will always refer to the same instance for the whole class hierarchy in a sealed trait.

I came to a solution by making the auto derived instance not implicit while keeping the Debug type parameter contravariant. I think it's now ready for review.

As zio-deriving is not published at the moment, I keep the magnolia implementation in this PR.

@alphaho alphaho marked this pull request as ready for review February 11, 2023 17:39
Copy link
Contributor

@adamgfraser adamgfraser left a comment

Choose a reason for hiding this comment

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

I just opened a PR dusting off some of my old work on derivation now that the issue with derivation of contravariant types on Scala 3 has been resolved. That provides a very nice experience for derivation of functional abstractions on Scala 3. However, Magnolia could be useful for users who are on Scala 2 or need to cross-compile.

@@ -193,6 +193,44 @@ lazy val experimentalTests = crossProject(JSPlatform, JVMPlatform, NativePlatfor
.nativeSettings(nativeSettings)
.enablePlugins(BuildInfoPlugin)

lazy val derivation = crossProject(JSPlatform, JVMPlatform, NativePlatform)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would name this magnolia as it is really providing interoperation with Magnolia and we may support interoperation with other derivation libraries or provide our own derivation solution in the future.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No problem. Let me rename it.

/**
* Derives a `Debug[Set[A]]` given a `Debug[A]`.
*/
implicit def SetDebug[A: Debug]: Debug[Set[A]] =
Copy link
Contributor

Choose a reason for hiding this comment

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

Great to add these instances! Can we add them to the tests too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure. Let me do it.


// need to keep it as not implicit, or the derivation would reference itself for the subtype's instances
// and runs into StackOverflow in the runtime
val debug: Debug[Entity] = DeriveDebug.gen[Entity]
Copy link
Contributor

Choose a reason for hiding this comment

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

This is pretty terrible. I think if you use -Xsource:3 it will fix this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let me have a try and see.

Copy link
Collaborator Author

@alphaho alphaho Feb 17, 2023

Choose a reason for hiding this comment

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

-Xsource:3 can't fix this either. :-(

The underlying problem of this can be implicit is because Debug is contravariant, and Magnolia/zio-deriving would resolve the subtypes' typeclass instance to this exact debug instance, instead of generating a new one.

When I add the implicit keyword under the magnolia implementation, the compiler would complain on this line that:

Implicit resolves to enclosing value debug
[warn]     implicit val debug: Debug[Entity] = DeriveDebug.gen[Entity]

And during runtime, as it always refer to itself, we will get a StackOverflowException in both Magnolia and zio-deriving.

But if I remove the implicit keyword, both libraries can generate a dedicated instance for a sealed trait's subtypes.

A workaround I can think of is to mimic DeriveGen from zio-test, making DeriveDebug a invariant typeclass, which can provide the Debug instance (like the invariant DeriveGen can provide a instance for Gen[-R, +A]). So that the client side can always declare the Debug instance implicit.
But the drawback of having the DeriveGen style implementation is that it can't provide very meaningful information, when it can't generate an instance. It's a painful experience every time I need to figure out what I miss if a hierarchy when it can't generate a DeriveGen.

How do you think about this solution?

object Entity {
case class Person(name: String, address: Address) extends Entity
object Person {
implicit val debug: Debug[Person] = DeriveDebug.gen[Person]
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to have an annotation for this. Something like @deriving(Debug).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could you elaborate more on this? Do you mean to have some annotations like @jsonHint so that we can fine tune the generated Debug instance?

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.

None yet

2 participants