From dccd24e90e03b3a9506c5e716f2caea79ca120fd Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 4 May 2023 00:30:07 +0200 Subject: [PATCH 1/3] Improve the "Backwards comptaible case classes" guide with Scala 2 and 3 specifics --- ...inary-compatibility-for-library-authors.md | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index a6d358ed14..64311e7b2c 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -178,11 +178,13 @@ 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 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 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 Example: @@ -191,6 +193,8 @@ Example: ~~~ scala // Mark the primary constructor as private case class Person private (name: String, age: Int) { + // Ensure the `copy` method is private + private def copy(name: String = name, age: Int = age) = new Person(name, age) // Create withXxx methods for every field, implemented by using the (private) copy method def withName(name: String): Person = copy(name = name) def withAge(age: Int): Person = copy(age = age) @@ -216,8 +220,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 %} @@ -256,12 +265,17 @@ Later in time, you can amend the original case class definition to, say, add an * add a new field `address` and a custom `withAddress` method, * update the public `apply` method in the companion object to initialize all the fields, * 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 2) add the new field to the `copy` method + * (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 new field to the `copy` method + private def copy(name: String = name, age: Int = age, address: Option[String] = None) = new Person(name, age, address) + // Add the `withXxx` method def withAddress(address: Option[String]) = copy(address = address) } @@ -280,6 +294,19 @@ 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 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 %} From 80939c85b30f43fcbe527d3583dd760798af42bb Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 4 May 2023 01:38:45 +0200 Subject: [PATCH 2/3] Add `apply` methods --- .../tutorials/binary-compatibility-for-library-authors.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 64311e7b2c..45f7f788c5 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -263,7 +263,7 @@ 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 2) add the new field to the `copy` method * (Scala 3) add a new case to the `fromProduct` method in the companion object @@ -274,7 +274,7 @@ Later in time, you can amend the original case class definition to, say, add an case class Person private (name: String, age: Int, address: Option[String]) { ... // Add the new field to the `copy` method - private def copy(name: String = name, age: Int = age, address: Option[String] = None) = new Person(name, age, address) + private def copy(name: String = name, age: Int = age, address: Option[String] = address) = new Person(name, age, address) // Add the `withXxx` method def withAddress(address: Option[String]) = copy(address = address) } @@ -282,6 +282,8 @@ 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) } ~~~ {% endtab %} @@ -294,6 +296,8 @@ 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 => From d6c5b3dfb4cb976041ef9e36631944cf8ed6b4e7 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Sat, 6 May 2023 00:45:29 +0200 Subject: [PATCH 3/3] Remove mentions of `copy` for Scala 2 --- .../tutorials/binary-compatibility-for-library-authors.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 45f7f788c5..a22165392f 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -178,11 +178,10 @@ 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 (for Scala 3, this makes the `copy` method of the class and `apply` in the companion object private as well) + * 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) * (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 @@ -193,8 +192,6 @@ Example: ~~~ scala // Mark the primary constructor as private case class Person private (name: String, age: Int) { - // Ensure the `copy` method is private - private def copy(name: String = name, age: Int = age) = new Person(name, age) // Create withXxx methods for every field, implemented by using the (private) copy method def withName(name: String): Person = copy(name = name) def withAge(age: Int): Person = copy(age = age) @@ -265,7 +262,6 @@ Later in time, you can amend the original case class definition to, say, add an * add a new field `address` and a custom `withAddress` method, * 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 2) add the new field to the `copy` method * (Scala 3) add a new case to the `fromProduct` method in the companion object {% tabs case_class_compat_4 class=tabs-scala-version %} @@ -273,8 +269,6 @@ Later in time, you can amend the original case class definition to, say, add an ~~~ scala case class Person private (name: String, age: Int, address: Option[String]) { ... - // Add the new field to the `copy` method - private def copy(name: String = name, age: Int = age, address: Option[String] = address) = new Person(name, age, address) // Add the `withXxx` method def withAddress(address: Option[String]) = copy(address = address) }