Skip to content

Commit 3ab5b4b

Browse files
committed
Final adjustments
1 parent f3e6c15 commit 3ab5b4b

File tree

1 file changed

+43
-8
lines changed

1 file changed

+43
-8
lines changed

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

+43-8
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ case class Person private (name: String, age: Int):
194194
// Create withXxx methods for every field, implemented by using the copy method
195195
def withName(name: String): Person = copy(name = name)
196196
def withAge(age: Int): Person = copy(age = age)
197+
197198
object Person:
198199
// Create a public constructor (which uses the primary constructor)
199200
def apply(name: String, age: Int) = new Person(name, age)
@@ -204,51 +205,85 @@ object Person:
204205
{% endtabs %}
205206
This class can be published in a library and used as follows:
206207

208+
{% tabs case_class_compat_2 %}
209+
{% tab 'Scala 2 and 3' %}
207210
~~~ scala
208211
// Create a new instance
209212
val alice = Person("Alice", 42)
210213
// Transform an instance
211214
println(alice.withAge(alice.age + 1)) // Person(Alice, 43)
212215
~~~
216+
{% endtab %}
217+
{% endtabs %}
213218

214219
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:
215220

221+
{% tabs case_class_compat_3 class=tabs-scala-version %}
222+
{% tab 'Scala 2' %}
223+
~~~ scala
224+
alice match {
225+
case person: Person => person.name
226+
}
227+
~~~
228+
{% endtab %}
229+
{% tab 'Scala 3' %}
216230
~~~ scala
217231
alice match
218232
case person: Person => person.name
219233
~~~
234+
{% endtab %}
235+
{% endtabs %}
236+
220237
Later in time, you can amend the original case class definition to, say, add an optional `address` field. You
221238
* 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 compilers currently emit the private constructors as public constructors in the bytecode (see [#12711](https://github.com/scala/bug/issues/12711) and [#16651](https://github.com/lampepfl/dotty/issues/16651)).
239+
* 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)).
223240

224-
{% tabs case_class_compat_2 %}
241+
{% tabs case_class_compat_4 %}
225242
{% tab 'Scala 3 Only' %}
226243
```scala
227244
case class Person private (name: String, age: Int, address: Option[String]):
228245
...
229-
// Add back the former primary constructor signature
230-
private[Person] def this(name: String, age: Int) = this(name, age, None)
231246
def withAddress(address: Option[String]) = copy(address = address)
232247
```
233248
{% endtab %}
234249
{% endtabs %}
235250

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:
251+
And, in your build definition:
252+
253+
{% tabs case_class_compat_5 %}
254+
{% tab 'sbt' %}
255+
~~~ scala
256+
import com.typesafe.tools.mima.core._
257+
mimaBinaryIssueFilters += ProblemFilters.exclude[DirectMissingMethodProblem]("Person.this")
258+
~~~
259+
{% endtab %}
260+
{% endtabs %}
261+
262+
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”.
263+
264+
> Note that an alternative solution, instead of adding a MiMa exclusion filter, consists of adding back the previous
265+
> constructor signatures as secondary constructors:
237266
> ~~~ scala
238-
> import com.typesafe.tools.mima.core._
239-
> mimaBinaryIssueFilters += ProblemFilters.exclude[DirectMissingMethodProblem]("Person.this")
267+
> case class Person private (name: String, age: Int, address: Option[String]):
268+
> ...
269+
> // Add back the former primary constructor signature
270+
> private[Person] def this(name: String, age: Int) = this(name, age, None)
240271
> ~~~
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”.
272+
242273
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.
243274
244275
The new field `address` can be used as follows:
245276
277+
{% tabs case_class_compat_6 %}
278+
{% tab 'Scala 2 and 3' %}
246279
~~~ scala
247280
// The public constructor sets the address to None by default.
248281
// To set the address, we call withAddress:
249282
val bob = Person("Bob", 21).withAddress(Some("Atlantic ocean"))
250283
println(bob.address)
251284
~~~
285+
{% endtab %}
286+
{% endtabs %}
252287
253288
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.
254289

0 commit comments

Comments
 (0)