-
Notifications
You must be signed in to change notification settings - Fork 1k
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 the "Backwards compatible case classes" guide with Scala 2 and 3 specifics #2788
base: main
Are you sure you want to change the base?
Conversation
Not to remove the need for your improvement, but:
makes it painfully obvious to me that this is not a viable strategy. At least not if you have Scala 2 in the mix. If you get any of those things wrong (by omitting it or even by writing the wrong signature), you end up with generated stuff anyway. And then you cannot get rid of it later. TBH, I think we should not recommend this for Scala 2 at all. Developers will inevitably get it wrong. I would get it wrong. |
Maybe that has been my plan all along 😈 But seriously now. I think there is a value for providing guidelines even for Scala 2. There are many eager volunteers maintaining popular libraries who would love to prevent their users from suffering from binary incompatibilities. These libraries are cross-compiled for both Scala 2 and 3. And having a guideline for both could help them. Anyway, I'm glad we now have a seed of an initiative to fix this pesky problem once and for all: https://contributors.scala-lang.org/t/can-we-make-adding-a-parameter-with-a-default-value-binary-compatible/6132 |
_overviews/tutorials/binary-compatibility-for-library-authors.md
Outdated
Show resolved
Hide resolved
* in Scala 2, you have to add the compiler option `-Xsource:3` | ||
* (Scala 2) you have to add the compiler option `-Xsource:3` | ||
* (Scala 2) define the `copy` method with all the current fields manually and set it as `private` | ||
* (Scala 2) define a private `unapply` method in the companion object (note that by doing that the case class loses the ability to be used as an extractor in match expressions) |
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 make this Scala 2 only? AFAIK it is also required in Scala 3.
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 according to @lihaoyi
unapply
: I think as of Scala 3 this will work right out of the box:unapply
no longer returnsOption[TupleN[...]]
as it did in Scala 2, and instead just returnsPerson
, with pattern matching just relying on the._1
._2
etc. fields to work. Thus, ap match{ case Person(first, last) => ???}
callsite compiled againstcase class Person(first: String, last: String)
should continue to work even whenPerson
has evolved intocase class Person(first: String, last: String, country: String = "unknown", number: Option[String] = None)
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 would be a source incompatibility, but if someone links an already compiled program with the new version of the case class, then they may get a run-time crash. I think it is preferable to recommend making unapply
private in both Scala 2 and 3.
* (Scala 2) you have to add the compiler option `-Xsource:3` | ||
* (Scala 2) define the `copy` method with all the current fields manually and set it as `private` | ||
* (Scala 2) define a private `unapply` method in the companion object (note that by doing that the case class loses the ability to be used as an extractor in match expressions) | ||
* (Scala 3) define a custom `fromProduct` method in the companion object |
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 one is interesting. We need to investigate further:
- what is the current state of mirrors (without defining
fromProduct
) - demonstrate in a project example that defining
fromProduct
is enough to get type class derivation working on case classes with a private primary constructor, and that it works as expected when fields are added.
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.
We can thank @armanbilge for discovering this
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 discussion needs to be resolved before we can advertise implementing fromProduct
.
Continuing from #2788 (comment):
I am currently still skeptical that e.g. consider evolving Now, suppose someone downstream is using the mirror to derive a JSON decoder for Specifically, mirror does not have a way to indicate which members were "added later" and what their default values are. It may be possible to emulate this in ad-hoc ways using |
The question is, whether defining custom But in any case, this is not a problem with binary compatibility, just to be clear. All the classes and methods from the old version are still present in the new. |
Not all decoders will be able to handle backwards compatibility in all possible cases. But many decoders can handle backwards compatibility in common cases. "Add a new param on the left with a default value" is perhaps the most common one, and many JSON decoders already handle that in a backwards compatible manner by instantiating the default during de-serialization if the new field is not received in the input. uPickle goes the extra step to avoid emitting the key-value pair during serialization if the current value is equal to the default, to provide backwards compatibility during serialization as well, but that's a design choice and other libraries do it differently. Being backwards compatible is certainly doable, within limits, and that's something that is up to the thing being derived to deal with |
Here is a scenario that breaks mirrors and
The conclusion is that Mirrors provide an equivalent to both |
@julienrf this is a great example where case classes can be evolved in a generally incompatible way that even MiMa wouldn't detect. (Maybe https://github.com/scalacenter/tasty-mima would be of help here?) But I think it's intuitive even to Scala beginners that removing fields from case class constructor is problematic. Anyway, in this PR we're trying to come up with a guide how to evolve case classes without breaking, not how to break them 😂 (there are thousand other ways that are even simpler, but I see how this example is interesting because of MiMa). I think @lihaoyi has a great point here:
There's only so much you as a case class author can do. Then you have to draw a line beyond which it's the case class user's (the one doing the deriving and/or the Type Class author) responsibility. |
Yes. I believe it's the |
@lihaoyi you've mentioned that
Could you tell us more about this? Are you doing this with uPickle on Scala 3? Are you using |
Hi all, what is the status of this PR? |
Thank you for pinging us @bishabosha. There are still at least two discussions that need to be resolved before we can move forward with this PR. @sideeffffect, would you be interested in investigating the points raised in those discussions? |
Continuation of
For Scala 3
fromProduct
is necessary: https://contributors.scala-lang.org/t/can-we-make-adding-a-parameter-with-a-default-value-binary-compatible/6132/8?u=sideeffffectunapply
shouldn't be necessary according to https://contributors.scala-lang.org/t/can-we-make-adding-a-parameter-with-a-default-value-binary-compatible/6132?u=sideeffffectFor Scala 2
copy
needs to be marked private manually (or maybe not? Add Scala 2 version of case classes that can evolve #2760 (comment) )/cc @julienrf