From cbe8a454c1cb561abdfee70a78f97b3557f045fa Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Thu, 30 Mar 2023 17:02:26 +0200 Subject: [PATCH] Add Scala 2 version of case classes that can evolve The same approach as with Scala 3 also works with Scala 2 if you add the `-Xsource:3` compiler option. --- ...inary-compatibility-for-library-authors.md | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 47261847d6..a6d358ed14 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -182,21 +182,39 @@ To achieve that, follow this pattern: * 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) * 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` Example: -{% tabs case_class_compat_1 %} -{% tab 'Scala 3 Only' %} +{% tabs case_class_compat_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} +~~~ scala +// Mark the primary constructor as private +case class Person private (name: String, age: Int) { + // 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) +} + +object Person { + // Create a public constructor (which uses the private primary constructor) + def apply(name: String, age: Int) = new Person(name, age) + // Make the extractor private + private def unapply(p: Person): Some[Person] = Some(p) +} +~~~ +{% endtab %} +{% tab 'Scala 3' %} ```scala // Mark the primary constructor as private case class Person private (name: String, age: Int): - // Create withXxx methods for every field, implemented by using the copy method + // 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) object Person: - // Create a public constructor (which uses the primary constructor) + // 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 @@ -239,8 +257,21 @@ Later in time, you can amend the original case class definition to, say, add an * 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)). -{% tabs case_class_compat_4 %} -{% tab 'Scala 3 Only' %} +{% tabs case_class_compat_4 class=tabs-scala-version %} +{% tab 'Scala 2' %} +~~~ scala +case class Person private (name: String, age: Int, address: Option[String]) { + ... + 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) +} +~~~ +{% endtab %} +{% tab 'Scala 3' %} ```scala case class Person private (name: String, age: Int, address: Option[String]): ... @@ -295,8 +326,19 @@ A regular case class not following this pattern would break its usage, because b Optionally, you can also add overloads of the `apply` method in the companion object to initialize more fields in one call. In our example, we can add an overload that also initializes the `address` field: -{% tabs case_class_compat_7 %} -{% tab 'Scala 3 Only' %} +{% tabs case_class_compat_7 class=tabs-scala-version %} +{% tab 'Scala 2' %} +~~~ scala +object Person { + // Original public constructor + def apply(name: String, age: Int): Person = new Person(name, age, None) + // Additional constructor that also sets the address + def apply(name: String, age: Int, address: String): Person = + new Person(name, age, Some(address)) +} +~~~ +{% endtab %} +{% tab 'Scala 3' %} ~~~ scala object Person: // Original public constructor