Skip to content

Commit 7668763

Browse files
Apply suggestions from code review
Co-authored-by: Julien Richard-Foy <[email protected]>
1 parent a52d7d6 commit 7668763

File tree

1 file changed

+43
-14
lines changed

1 file changed

+43
-14
lines changed

_overviews/tutorials/binary-compatibility-for-library-authors.md

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -175,52 +175,81 @@ Again, we recommend using MiMa to double-check that you have not broken binary c
175175

176176
### Changing a case class definition in a backwards-compatible manner
177177

178-
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_.
178+
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`.
179179

180180
To achieve that, follow this pattern:
181-
* make the constructor private (this makes private the `copy` method of the class)
182-
* define a private `unapply` function in the companion object (note that by doing that the case class loses the ability to be used in an extractor pattern match)
183-
* for all the fields, define `withXXX` methods on the case class that create a new instance with the respective field changed
184-
* define custom `apply` factory method(s) in the companion object (these can use the private constructor)
181+
* make the primary constructor private (this makes private the `copy` method of the class)
182+
* 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)
183+
* 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)
184+
* create a public constructor by defining an `apply` method in the companion object (it can use the private constructor)
185185

186186
Example:
187187

188188
{% tabs case_class_compat_1 %}
189189
{% tab 'Scala 3 Only' %}
190190

191191
```scala
192+
// Mark the primary constructor as private
192193
case class Person private (name: String, age: Int):
194+
// Create withXxx methods for every field, implemented by using the copy method
193195
def withName(name: String): Person = copy(name = name)
194196
def withAge(age: Int): Person = copy(age = age)
195197
object Person:
198+
// Create a public constructor (which uses the primary constructor)
196199
def apply(name: String, age: Int) = new Person(name, age)
200+
// Make the extractor private
197201
private def unapply(p: Person) = p
198202
```
199203
{% endtab %}
200204
{% endtabs %}
205+
This class can be published in a library and used as follows:
201206

202-
Later in time, you can amend the original case class definition. You
203-
* add a new field `address`,
204-
* add a new constructor, private to the companion object, with the original fields
205-
* add a custom `withAddress` method and
206-
* add an `apply` factory method to the companion.
207+
~~~ scala
208+
// Create a new instance
209+
val alice = Person("Alice", 42)
210+
// Transform an instance
211+
println(alice.withAge(alice.age + 1)) // Person(Alice, 43)
212+
~~~
213+
214+
If you try to use `Person` as an extractor in a match expression, it will fail with a message like “method unapply cannot be accessed as a member of Person.type”. Instead, you can use it as a typed pattern:
215+
216+
~~~ scala
217+
alice match
218+
case person: Person => person.name
219+
~~~
220+
Later in time, you can amend the original case class definition to, say, add an optional `address` field. You
221+
* add a new field `address` and a custom `withAddress` method,
222+
* add the former constructor signature as a secondary constructor, private to the companion object. This step is necessary because the public `apply` method in the companion object calls the former constructor, which was effectively public in the bytecode produced by the compiler.
207223

208224
{% tabs case_class_compat_2 %}
209225
{% tab 'Scala 3 Only' %}
210226
```scala
211227
case class Person private (name: String, age: Int, address: Option[String]):
212228
...
229+
// Add back the former primary constructor signature
213230
private[Person] def this(name: String, age: Int): Person = this(name, age, None)
214-
def withAddress(address: String) = copy(address = address)
215-
object Person:
216-
...
217-
def apply(name: String, age: Int, address: String) = new Person(name, age, Some(address))
231+
def withAddress(address: Option[String]) = copy(address = address)
218232
```
219233
{% endtab %}
220234
{% endtabs %}
221235

236+
> Note that an alternative solution, instead of adding back the previous constructor signatures as secondary constructors, consists of adding a [MiMa filter](https://github.com/lightbend/mima#filtering-binary-incompatibilities) to simply ignore the problem. Even though the constructors are effectively public in the bytecode, they can’t be called from Scala programs (but they could be called by Java programs). In an sbt build definition you would add the following setting:
237+
> ~~~ scala
238+
> import com.typesafe.tools.mima.core._
239+
> mimaBinaryIssueFilters += ProblemFilters.exclude[DirectMissingMethodProblem]("Person.this")
240+
> ~~~
241+
> Otherwise, MiMa would fail with an error like “method this(java.lang.String,Int)Unit in class Person does not have a correspondent in current version”.
222242
The original users can use the case class `Person` as before, all the methods that existed before are present unmodified after this change, thus the compatibility with the existing usage is maintained.
223243
244+
The new field `address` can be used as follows:
245+
246+
~~~ scala
247+
// The public constructor sets the address to None by default.
248+
// To set the address, we call withAddress:
249+
val bob = Person("Bob", 21).withAddress(Some("Atlantic ocean"))
250+
println(bob.address)
251+
~~~
252+
224253
A regular case class not following this pattern would break its usage, because by adding a new field changes some methods (which could be used by somebody else), for example `copy` or the constructor itself.
225254

226255
## Versioning Scheme - Communicating compatibility breakages

0 commit comments

Comments
 (0)