-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -178,11 +178,12 @@ Again, we recommend using MiMa to double-check that you have not broken binary c | |
Sometimes, it is desirable to change the definition of a case class (adding and/or removing fields) while still staying backwards-compatible with the existing usage of the case class, i.e. not breaking the so-called _binary compatibility_. The first question you should ask yourself is “do you need a _case_ class?” (as opposed to a regular class, which can be easier to evolve in a binary compatible way). A good reason for using a case class is when you need a structural implementation of `equals` and `hashCode`. | ||
|
||
To achieve that, follow this pattern: | ||
* make the primary constructor private (this makes the `copy` method of the class private as well) | ||
* define a private `unapply` function in the companion object (note that by doing that the case class loses the ability to be used as an extractor in match expressions) | ||
* make the primary constructor private (for Scala 3, this makes the `copy` method of the class and `apply` in the companion object private as well, for Scala 2 you get this behavior using `-Xsource:3`) | ||
* for all the fields, define `withXXX` methods on the case class that create a new instance with the respective field changed (you can use the private `copy` method to implement them) | ||
* create a public constructor by defining an `apply` method in the companion object (it can use the private constructor) | ||
* 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 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 commentThe reason will be displayed to describe this comment to others. Learn more. That one is interesting. We need to investigate further:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. That discussion needs to be resolved before we can advertise implementing |
||
|
||
Example: | ||
|
||
|
@@ -216,8 +217,13 @@ case class Person private (name: String, age: Int): | |
object Person: | ||
// Create a public constructor (which uses the private primary constructor) | ||
def apply(name: String, age: Int): Person = new Person(name, age) | ||
// Make the extractor private | ||
private def unapply(p: Person) = p | ||
// Implement a custom `fromProduct` | ||
def fromProduct(p: Product): Person = p.productArity match | ||
case 2 => | ||
Person( | ||
p.productElement(0).asInstanceOf[String], | ||
p.productElement(1).asInstanceOf[Int], | ||
) | ||
``` | ||
{% endtab %} | ||
{% endtabs %} | ||
|
@@ -254,20 +260,24 @@ alice match | |
|
||
Later in time, you can amend the original case class definition to, say, add an optional `address` field. You | ||
* add a new field `address` and a custom `withAddress` method, | ||
* update the public `apply` method in the companion object to initialize all the fields, | ||
* add a new `apply` method to the companion object to initialize all the fields and adjust the old `apply`, | ||
* tell MiMa to [ignore](https://github.com/lightbend/mima#filtering-binary-incompatibilities) changes to the class constructor. This step is necessary because MiMa does not yet ignore changes in private class constructor signatures (see [#738](https://github.com/lightbend/mima/issues/738)). | ||
* (Scala 3) add a new case to the `fromProduct` method in the companion object | ||
|
||
{% tabs case_class_compat_4 class=tabs-scala-version %} | ||
{% tab 'Scala 2' %} | ||
~~~ scala | ||
case class Person private (name: String, age: Int, address: Option[String]) { | ||
... | ||
// Add the `withXxx` method | ||
def withAddress(address: Option[String]) = copy(address = address) | ||
} | ||
|
||
object Person { | ||
// Update the public constructor to also initialize the address field | ||
def apply(name: String, age: Int): Person = new Person(name, age, None) | ||
// Add a new `apply` with the address field | ||
def apply(name: String, age: Int, address: Option[String]): Person = new Person(name, age, address) | ||
} | ||
~~~ | ||
{% endtab %} | ||
|
@@ -280,6 +290,21 @@ case class Person private (name: String, age: Int, address: Option[String]): | |
object Person: | ||
// Update the public constructor to also initialize the address field | ||
def apply(name: String, age: Int): Person = new Person(name, age, None) | ||
// Add a new `apply` with the address field | ||
def apply(name: String, age: Int, address: Option[String]): Person = new Person(name, age, address) | ||
// Add a new case to `fromProduct` | ||
def fromProduct(p: Product): Person = p.productArity match | ||
case 2 => | ||
Person( | ||
p.productElement(0).asInstanceOf[String], | ||
p.productElement(1).asInstanceOf[Int], | ||
) | ||
case 3 => | ||
Person( | ||
p.productElement(0).asInstanceOf[String], | ||
p.productElement(1).asInstanceOf[Int], | ||
p.productElement(2).asInstanceOf[Option[String]], | ||
) | ||
``` | ||
{% endtab %} | ||
{% endtabs %} | ||
|
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
https://contributors.scala-lang.org/t/can-we-make-adding-a-parameter-with-a-default-value-binary-compatible/6132?u=sideeffffect
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.