Skip to content
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

Add Scala 2 version of case classes that can evolve #2760

Merged
merged 1 commit into from
Mar 31, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions _overviews/tutorials/binary-compatibility-for-library-authors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]):
...
Expand Down Expand Up @@ -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
Expand Down