diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index a6d358ed14..a22165392f 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -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 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 %}