From e54f7f1d8060418ddad34ba55813dce74345317c Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Wed, 21 Dec 2022 17:00:17 +0100 Subject: [PATCH 01/26] Document the patter for evolving case classes in a compatible manner --- .../scala3-book/domain-modeling-tools.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 762dd1bc2e..2beb8dde31 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -1107,6 +1107,56 @@ As mentioned, case classes support functional programming (FP): This process can be referred to as “update as you copy.” - Having an `unapply` method auto-generated for you also lets case classes be used in advanced ways with pattern matching. +### Changing case class definition in a compatible manner + +Sometimes it is desirable to be able to change the definition of a case class (adding and/or removing fields) while still staying compatible with the users of the case class, i.e. not breaking the so called _binary compatibility_. + +To achieve that, use this pattern: + * make the constructor `private` + * define `private` `unapply` function in the companion object + * define `withXXX` methods on the case class that create a new instance with the respective field changed + * define custom `apply` factory method(s) in the companion object (these can use the private constructor) + +Example: + +{% tabs case_class_compat_1 %} +{% tab 'Scala 3 Only' %} + +```scala +case class Person private (name: String, age: Int): + def withName(name: String) = copy(name = name) + def withAge(age: Int) = copy(age = age) + +object Person: + def apply(name: String, age: Int) = new Person(name, age) + private def unapply(p: Person) = p +``` +{% endtab %} +{% endtabs %} + +Later in time, you can ammend the original case class definition. You + * add a new field `address`, + * add a custom `withAddress` method and + * add an `apply` factory method to the companion. + +{% tabs case_class_compat_2 %} +{% tab 'Scala 3 Only' %} +```scala +case class Person private (name: String, age: Int, address: String = ""): + ... + def withAddress(address: String) = copy(address = address) + +object Person: + ... + def apply(name: String, age: Int, address: String) = new Person(name, age, address) +``` +{% endtab %} +{% endtabs %} + +The original users can use the case class `Person` as before, all the methods that existed before are present after this change, thus the compatibility with them is maintained. + +A regular case class not following this pattern would break its users, because by adding a new field some methods (which could be used by somebody else) change, for example `copy` or the constructor itself. + {% comment %} NOTE: We can use this following text, if desired. If it’s used, it needs to be updated a little bit. From 5c8df689594390398ccd788ed8757e1b1b72979c Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Wed, 21 Dec 2022 17:04:22 +0100 Subject: [PATCH 02/26] Update domain-modeling-tools.md --- _overviews/scala3-book/domain-modeling-tools.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 2beb8dde31..8c18361926 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -1111,7 +1111,7 @@ As mentioned, case classes support functional programming (FP): Sometimes it is desirable to be able to change the definition of a case class (adding and/or removing fields) while still staying compatible with the users of the case class, i.e. not breaking the so called _binary compatibility_. -To achieve that, use this pattern: +To achieve that, follow this pattern: * make the constructor `private` * define `private` `unapply` function in the companion object * define `withXXX` methods on the case class that create a new instance with the respective field changed @@ -1153,7 +1153,7 @@ object Person: {% endtab %} {% endtabs %} -The original users can use the case class `Person` as before, all the methods that existed before are present after this change, thus the compatibility with them is maintained. +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 users is maintained. A regular case class not following this pattern would break its users, because by adding a new field some methods (which could be used by somebody else) change, for example `copy` or the constructor itself. From 0361062e3e91a39fadbe1c3fd4cc10460e979479 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Wed, 21 Dec 2022 17:20:53 +0100 Subject: [PATCH 03/26] Update binary-compatibility-for-library-authors.md --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index ad0d34f913..fd969d1da1 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -167,7 +167,7 @@ For example, the use of these language features are a common source of binary co in library releases: * Default parameter values for methods or classes -* Case classes +* Case classes (work around the issue with [this pattern]({{ site.baseurl }}/scala3/book/domain-modeling-tools.html#changing-case-class-definition-in-a-compatible-manner)) You can find detailed explanations, runnable examples and tips to maintain binary compatibility in [Binary Compatibility Code Examples & Explanation](https://github.com/jatcwang/binary-compatibility-guide). From 3b1fc578f1aa71b7fb7d760ebb2fe16ba1205503 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Wed, 21 Dec 2022 23:33:28 +0100 Subject: [PATCH 04/26] Update binary-compatibility-for-library-authors.md --- ...inary-compatibility-for-library-authors.md | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index fd969d1da1..77d6efd19b 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -167,12 +167,60 @@ For example, the use of these language features are a common source of binary co in library releases: * Default parameter values for methods or classes -* Case classes (work around the issue with [this pattern]({{ site.baseurl }}/scala3/book/domain-modeling-tools.html#changing-case-class-definition-in-a-compatible-manner)) +* Case classes You can find detailed explanations, runnable examples and tips to maintain binary compatibility in [Binary Compatibility Code Examples & Explanation](https://github.com/jatcwang/binary-compatibility-guide). Again, we recommend using MiMa to double-check that you have not broken binary compatibility after making changes. +### Changing case class definition in a compatible manner + +Sometimes it is desirable to be able to change the definition of a case class (adding and/or removing fields) while still staying compatible with the users of the case class, i.e. not breaking the so called _binary compatibility_. + +To achieve that, follow this pattern: + * make the constructor `private` + * define `private` `unapply` function in the companion object (note that by doing so you loose the ability to use the case class in a pattern match) + * define `withXXX` methods on the case class that create a new instance with the respective field changed + * define custom `apply` factory method(s) in the companion object (these can use the private constructor) + +Example: + +{% tabs case_class_compat_1 %} +{% tab 'Scala 3 Only' %} + +```scala +case class Person private (name: String, age: Int): + def withName(name: String) = copy(name = name) + def withAge(age: Int) = copy(age = age) +object Person: + def apply(name: String, age: Int) = new Person(name, age) + private def unapply(p: Person) = p +``` +{% endtab %} +{% endtabs %} + +Later in time, you can ammend the original case class definition. You + * add a new field `address`, + * add a custom `withAddress` method and + * add an `apply` factory method to the companion. + +{% tabs case_class_compat_2 %} +{% tab 'Scala 3 Only' %} +```scala +case class Person private (name: String, age: Int, address: String = ""): + ... + def withAddress(address: String) = copy(address = address) +object Person: + ... + def apply(name: String, age: Int, address: String) = new Person(name, age, address) +``` +{% endtab %} +{% endtabs %} + +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 users is maintained. + +A regular case class not following this pattern would break its users, because by adding a new field some methods (which could be used by somebody else) change, for example `copy` or the constructor itself. + ## Versioning Scheme - Communicating compatibility breakages Library authors use versioning schemes to communicate compatibility guarantees between library releases to their users. Versioning schemes like [Semantic Versioning](https://semver.org/) (SemVer) allow From 6b8b1573838c2b22e8fdd07e70059626a5339620 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Wed, 21 Dec 2022 23:34:10 +0100 Subject: [PATCH 05/26] Update domain-modeling-tools.md --- .../scala3-book/domain-modeling-tools.md | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 8c18361926..762dd1bc2e 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -1107,56 +1107,6 @@ As mentioned, case classes support functional programming (FP): This process can be referred to as “update as you copy.” - Having an `unapply` method auto-generated for you also lets case classes be used in advanced ways with pattern matching. -### Changing case class definition in a compatible manner - -Sometimes it is desirable to be able to change the definition of a case class (adding and/or removing fields) while still staying compatible with the users of the case class, i.e. not breaking the so called _binary compatibility_. - -To achieve that, follow this pattern: - * make the constructor `private` - * define `private` `unapply` function in the companion object - * define `withXXX` methods on the case class that create a new instance with the respective field changed - * define custom `apply` factory method(s) in the companion object (these can use the private constructor) - -Example: - -{% tabs case_class_compat_1 %} -{% tab 'Scala 3 Only' %} - -```scala -case class Person private (name: String, age: Int): - def withName(name: String) = copy(name = name) - def withAge(age: Int) = copy(age = age) - -object Person: - def apply(name: String, age: Int) = new Person(name, age) - private def unapply(p: Person) = p -``` -{% endtab %} -{% endtabs %} - -Later in time, you can ammend the original case class definition. You - * add a new field `address`, - * add a custom `withAddress` method and - * add an `apply` factory method to the companion. - -{% tabs case_class_compat_2 %} -{% tab 'Scala 3 Only' %} -```scala -case class Person private (name: String, age: Int, address: String = ""): - ... - def withAddress(address: String) = copy(address = address) - -object Person: - ... - def apply(name: String, age: Int, address: String) = new Person(name, age, address) -``` -{% endtab %} -{% endtabs %} - -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 users is maintained. - -A regular case class not following this pattern would break its users, because by adding a new field some methods (which could be used by somebody else) change, for example `copy` or the constructor itself. - {% comment %} NOTE: We can use this following text, if desired. If it’s used, it needs to be updated a little bit. From 75b59371f8f591016554ca09f19250549f6238e2 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Wed, 21 Dec 2022 23:38:22 +0100 Subject: [PATCH 06/26] Update binary-compatibility-for-library-authors.md --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 77d6efd19b..7c7b8abf39 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -179,7 +179,7 @@ Sometimes it is desirable to be able to change the definition of a case class (a To achieve that, follow this pattern: * make the constructor `private` - * define `private` `unapply` function in the companion object (note that by doing so you loose the ability to use the case class in a pattern match) + * define `private` `unapply` function in the companion object (note that by doing that the case class looses the ability to be used in a pattern match) * define `withXXX` methods on the case class that create a new instance with the respective field changed * define custom `apply` factory method(s) in the companion object (these can use the private constructor) From 1118fb3e47a5ed698118ca0f7032770844f830c8 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 00:03:54 +0100 Subject: [PATCH 07/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Kozłowski --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 7c7b8abf39..5ff4b4a8a2 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -179,7 +179,7 @@ Sometimes it is desirable to be able to change the definition of a case class (a To achieve that, follow this pattern: * make the constructor `private` - * define `private` `unapply` function in the companion object (note that by doing that the case class looses the ability to be used in a pattern match) + * define a private `unapply` function in the companion object (note that by doing that the case class loses the ability to be used in a pattern match) * define `withXXX` methods on the case class that create a new instance with the respective field changed * define custom `apply` factory method(s) in the companion object (these can use the private constructor) From 64776be79db4d3c18562bdf42619f77ff101e1d5 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 00:05:12 +0100 Subject: [PATCH 08/26] Update binary-compatibility-for-library-authors.md --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 5ff4b4a8a2..f038f6f274 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -178,7 +178,7 @@ Again, we recommend using MiMa to double-check that you have not broken binary c Sometimes it is desirable to be able to change the definition of a case class (adding and/or removing fields) while still staying compatible with the users of the case class, i.e. not breaking the so called _binary compatibility_. To achieve that, follow this pattern: - * make the constructor `private` + * make the constructor private * define a private `unapply` function in the companion object (note that by doing that the case class loses the ability to be used in a pattern match) * define `withXXX` methods on the case class that create a new instance with the respective field changed * define custom `apply` factory method(s) in the companion object (these can use the private constructor) From b583d464b43806516581ce0213a8f238cfa49dd1 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 13:37:01 +0100 Subject: [PATCH 09/26] Update binary-compatibility-for-library-authors.md --- .../tutorials/binary-compatibility-for-library-authors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index f038f6f274..e5fd3b11cd 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -179,7 +179,7 @@ Sometimes it is desirable to be able to change the definition of a case class (a To achieve that, follow this pattern: * make the constructor private - * define a private `unapply` function in the companion object (note that by doing that the case class loses the ability to be used in a pattern match) + * 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) * define `withXXX` methods on the case class that create a new instance with the respective field changed * define custom `apply` factory method(s) in the companion object (these can use the private constructor) @@ -219,7 +219,7 @@ object Person: 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 users is maintained. -A regular case class not following this pattern would break its users, because by adding a new field some methods (which could be used by somebody else) change, for example `copy` or the constructor itself. +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. ## Versioning Scheme - Communicating compatibility breakages From a6d4fc00a7cb3b535406e52d9ea5e4d7e9a2038e Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:47:46 +0100 Subject: [PATCH 10/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index e5fd3b11cd..b79e2fb48a 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -173,7 +173,7 @@ You can find detailed explanations, runnable examples and tips to maintain binar Again, we recommend using MiMa to double-check that you have not broken binary compatibility after making changes. -### Changing case class definition in a compatible manner +### Changing a case class definition in a backwards-compatible manner Sometimes it is desirable to be able to change the definition of a case class (adding and/or removing fields) while still staying compatible with the users of the case class, i.e. not breaking the so called _binary compatibility_. From 85bf8e7a8efd00ab408654b6dc1378f40b571089 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:48:02 +0100 Subject: [PATCH 11/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index b79e2fb48a..c99891882f 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -175,7 +175,7 @@ Again, we recommend using MiMa to double-check that you have not broken binary c ### Changing a case class definition in a backwards-compatible manner -Sometimes it is desirable to be able to change the definition of a case class (adding and/or removing fields) while still staying compatible with the users of the case class, i.e. not breaking the so called _binary compatibility_. +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_. To achieve that, follow this pattern: * make the constructor private From 359219e39b3cc65ff5a369f62e72e5306cc5eeed Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:48:24 +0100 Subject: [PATCH 12/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index c99891882f..38350d5413 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -178,7 +178,7 @@ 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_. To achieve that, follow this pattern: - * make the constructor private + * make the constructor private (this also makes private the `copy` method of the class) * 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) * define `withXXX` methods on the case class that create a new instance with the respective field changed * define custom `apply` factory method(s) in the companion object (these can use the private constructor) From 25af00fdd1d00c0556f3b2bdcd67247c9fdffad4 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:48:35 +0100 Subject: [PATCH 13/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 38350d5413..72e260cd11 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -180,7 +180,7 @@ Sometimes, it is desirable to change the definition of a case class (adding and/ To achieve that, follow this pattern: * make the constructor private (this also makes private the `copy` method of the class) * 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) - * define `withXXX` methods on the case class that create a new instance with the respective field changed + * for all the fields, define `withXXX` methods on the case class that create a new instance with the respective field changed * define custom `apply` factory method(s) in the companion object (these can use the private constructor) Example: From b32895846bb5e7ec72b4ca2f7798124267a76444 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:48:59 +0100 Subject: [PATCH 14/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 72e260cd11..b2c9a15f38 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -190,7 +190,7 @@ Example: ```scala case class Person private (name: String, age: Int): - def withName(name: String) = copy(name = name) + def withName(name: String): Person = copy(name = name) def withAge(age: Int) = copy(age = age) object Person: def apply(name: String, age: Int) = new Person(name, age) From caaa5326e1ba0220f0d1fd0ac37ec65bf00af643 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:49:11 +0100 Subject: [PATCH 15/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index b2c9a15f38..f04a0d643b 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -191,7 +191,7 @@ Example: ```scala case class Person private (name: String, age: Int): def withName(name: String): Person = copy(name = name) - def withAge(age: Int) = copy(age = age) + def withAge(age: Int): Person = copy(age = age) object Person: def apply(name: String, age: Int) = new Person(name, age) private def unapply(p: Person) = p From 9afdd845a5f59e600901afd78d974e176e86d4a3 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:49:21 +0100 Subject: [PATCH 16/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index f04a0d643b..02a1a6926e 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -199,7 +199,7 @@ object Person: {% endtab %} {% endtabs %} -Later in time, you can ammend the original case class definition. You +Later in time, you can amend the original case class definition. You * add a new field `address`, * add a custom `withAddress` method and * add an `apply` factory method to the companion. From 7806dbd51fd8bdfb9d67a9bdc63284c515a380b7 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 15:51:33 +0100 Subject: [PATCH 17/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 02a1a6926e..1d2fefd9d6 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -217,7 +217,7 @@ object Person: {% endtab %} {% endtabs %} -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 users is maintained. +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. 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. From a764a9c7771a4dd20475f4bcda01bae988db75ef Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 16:09:14 +0100 Subject: [PATCH 18/26] Update binary-compatibility-for-library-authors.md --- .../tutorials/binary-compatibility-for-library-authors.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 1d2fefd9d6..e40cf25d05 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -178,7 +178,7 @@ 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_. To achieve that, follow this pattern: - * make the constructor private (this also makes private the `copy` method of the class) + * make the constructor private (this makes private the `copy` method of the class) * 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) * for all the fields, define `withXXX` methods on the case class that create a new instance with the respective field changed * define custom `apply` factory method(s) in the companion object (these can use the private constructor) @@ -201,18 +201,20 @@ object Person: Later in time, you can amend the original case class definition. You * add a new field `address`, + * add a private constructor with the original fields * add a custom `withAddress` method and * add an `apply` factory method to the companion. {% tabs case_class_compat_2 %} {% tab 'Scala 3 Only' %} ```scala -case class Person private (name: String, age: Int, address: String = ""): +case class Person private (name: String, age: Int, address: Option[String]): ... + private[Person] def this(name: String, age: Int): Person = this(name, age, None) def withAddress(address: String) = copy(address = address) object Person: ... - def apply(name: String, age: Int, address: String) = new Person(name, age, address) + def apply(name: String, age: Int, address: String) = new Person(name, age, Some(address)) ``` {% endtab %} {% endtabs %} From a52d7d68c18c443a11f1bb1ba7dbd71aadfbbb7d Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Thu, 22 Dec 2022 16:10:35 +0100 Subject: [PATCH 19/26] Update binary-compatibility-for-library-authors.md --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index e40cf25d05..9d69590a7f 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -201,7 +201,7 @@ object Person: Later in time, you can amend the original case class definition. You * add a new field `address`, - * add a private constructor with the original fields + * add a new constructor, private to the companion object, with the original fields * add a custom `withAddress` method and * add an `apply` factory method to the companion. From 76687632a6278ad3227c986e81bfc05891476468 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Sun, 1 Jan 2023 20:43:59 +0100 Subject: [PATCH 20/26] Apply suggestions from code review Co-authored-by: Julien Richard-Foy --- ...inary-compatibility-for-library-authors.md | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 9d69590a7f..934e952204 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -175,13 +175,13 @@ Again, we recommend using MiMa to double-check that you have not broken binary c ### Changing a case class definition in a backwards-compatible manner -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_. +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 constructor private (this makes private the `copy` method of the class) - * 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) - * for all the fields, define `withXXX` methods on the case class that create a new instance with the respective field changed - * define custom `apply` factory method(s) in the companion object (these can use the private constructor) + * make the primary constructor private (this makes private the `copy` method of the class) + * 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) Example: @@ -189,38 +189,67 @@ Example: {% tab 'Scala 3 Only' %} ```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 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) def apply(name: String, age: Int) = new Person(name, age) + // Make the extractor private private def unapply(p: Person) = p ``` {% endtab %} {% endtabs %} +This class can be published in a library and used as follows: -Later in time, you can amend the original case class definition. You - * add a new field `address`, - * add a new constructor, private to the companion object, with the original fields - * add a custom `withAddress` method and - * add an `apply` factory method to the companion. +~~~ scala +// Create a new instance +val alice = Person("Alice", 42) +// Transform an instance +println(alice.withAge(alice.age + 1)) // Person(Alice, 43) +~~~ + +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: + +~~~ scala +alice match + case person: Person => person.name +~~~ +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, + * 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. {% tabs case_class_compat_2 %} {% tab 'Scala 3 Only' %} ```scala case class Person private (name: String, age: Int, address: Option[String]): ... + // Add back the former primary constructor signature private[Person] def this(name: String, age: Int): Person = this(name, age, None) - def withAddress(address: String) = copy(address = address) -object Person: - ... - def apply(name: String, age: Int, address: String) = new Person(name, age, Some(address)) + def withAddress(address: Option[String]) = copy(address = address) ``` {% endtab %} {% endtabs %} +> 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: +> ~~~ scala +> import com.typesafe.tools.mima.core._ +> mimaBinaryIssueFilters += ProblemFilters.exclude[DirectMissingMethodProblem]("Person.this") +> ~~~ +> 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”. 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. +The new field `address` can be used as follows: + +~~~ scala +// The public constructor sets the address to None by default. +// To set the address, we call withAddress: +val bob = Person("Bob", 21).withAddress(Some("Atlantic ocean")) +println(bob.address) +~~~ + 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. ## Versioning Scheme - Communicating compatibility breakages From 293a0bd1468bb5cf26a303febda822cb5734efdd Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Sun, 1 Jan 2023 22:36:50 +0100 Subject: [PATCH 21/26] Update binary-compatibility-for-library-authors.md fix typo --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 934e952204..5dc1a56d17 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -227,7 +227,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 back the former primary constructor signature - private[Person] def this(name: String, age: Int): Person = this(name, age, None) + private[Person] def this(name: String, age: Int) = this(name, age, None) def withAddress(address: Option[String]) = copy(address = address) ``` {% endtab %} From 5c1d9b3b205ddd023977813aafa7e9db67f7a634 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Tue, 10 Jan 2023 18:26:35 +0100 Subject: [PATCH 22/26] Update _overviews/tutorials/binary-compatibility-for-library-authors.md Co-authored-by: Julien Richard-Foy --- .../tutorials/binary-compatibility-for-library-authors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 5dc1a56d17..974eed4248 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -219,7 +219,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, - * 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. + * 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)). {% tabs case_class_compat_2 %} {% tab 'Scala 3 Only' %} From 4f548e986d418e8a522c7dfdec071d8225597038 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Tue, 10 Jan 2023 18:29:51 +0100 Subject: [PATCH 23/26] Apply suggestions from code review Co-authored-by: Julien Richard-Foy --- ...inary-compatibility-for-library-authors.md | 65 ++++++++++++++++--- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 974eed4248..6779d344ae 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -185,10 +185,27 @@ To achieve that, follow this pattern: Example: -{% tabs case_class_compat_1 %} -{% tab 'Scala 3 Only' %} +{% tabs case_class_compat_1 class=tabs-scala-version %} -```scala +{% 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 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) + 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 @@ -198,38 +215,64 @@ object Person: // Create a public constructor (which uses the primary constructor) def apply(name: String, age: Int) = new Person(name, age) // Make the extractor private - private def unapply(p: Person) = p -``` + private def unapply(p: Person): Person = p +~~~ {% endtab %} + {% endtabs %} This class can be published in a library and used as follows: +{% tabs case_class_compat_1_b %} +{% tab 'Scala 2 and 3' %} ~~~ scala // Create a new instance val alice = Person("Alice", 42) // Transform an instance println(alice.withAge(alice.age + 1)) // Person(Alice, 43) ~~~ +{% endtab %} +{% endtabs %} 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: +{% tabs case_class_compat_1_c class=tabs-scala-version %} +{% tab 'Scala 2' %} +~~~ scala +alice match { + case person: Person => person.name +} +~~~ +{% endtab %} +{% tab 'Scala 3' %} ~~~ scala alice match case person: Person => person.name ~~~ +{% endtab %} +{% endtabs %} 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, * 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)). -{% tabs case_class_compat_2 %} -{% tab 'Scala 3 Only' %} -```scala +{% tabs case_class_compat_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} +~~~ scala +case class Person private (name: String, age: Int, address: Option[String]) { + ... + // Add back the former primary constructor signature + private[Person] def this(name: String, age: Int) = this(name, age, None) + def withAddress(address: Option[String]) = copy(address = address) +} +~~~ +{% endtab %} +{% tab 'Scala 3' %} +~~~ scala case class Person private (name: String, age: Int, address: Option[String]): ... // Add back the former primary constructor signature private[Person] def this(name: String, age: Int) = this(name, age, None) def withAddress(address: Option[String]) = copy(address = address) -``` +~~~ {% endtab %} {% endtabs %} @@ -243,12 +286,16 @@ The original users can use the case class `Person` as before, all the methods th The new field `address` can be used as follows: +{% tabs case_class_compat_3 %} +{% tab 'Scala 2 and 3' %} ~~~ scala // The public constructor sets the address to None by default. // To set the address, we call withAddress: val bob = Person("Bob", 21).withAddress(Some("Atlantic ocean")) println(bob.address) ~~~ +{% endtab %} +{% endtabs %} 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. From f3e6c1503694abc873cf4063fed89e43ec176724 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Fri, 13 Jan 2023 08:57:37 +0100 Subject: [PATCH 24/26] Revert "Apply suggestions from code review" This reverts commit 4f548e986d418e8a522c7dfdec071d8225597038. --- ...inary-compatibility-for-library-authors.md | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 6779d344ae..974eed4248 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -185,27 +185,10 @@ To achieve that, follow this pattern: Example: -{% tabs case_class_compat_1 class=tabs-scala-version %} +{% tabs case_class_compat_1 %} +{% tab 'Scala 3 Only' %} -{% 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 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) - 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 +```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 @@ -215,64 +198,38 @@ object Person: // Create a public constructor (which uses the primary constructor) def apply(name: String, age: Int) = new Person(name, age) // Make the extractor private - private def unapply(p: Person): Person = p -~~~ + private def unapply(p: Person) = p +``` {% endtab %} - {% endtabs %} This class can be published in a library and used as follows: -{% tabs case_class_compat_1_b %} -{% tab 'Scala 2 and 3' %} ~~~ scala // Create a new instance val alice = Person("Alice", 42) // Transform an instance println(alice.withAge(alice.age + 1)) // Person(Alice, 43) ~~~ -{% endtab %} -{% endtabs %} 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: -{% tabs case_class_compat_1_c class=tabs-scala-version %} -{% tab 'Scala 2' %} -~~~ scala -alice match { - case person: Person => person.name -} -~~~ -{% endtab %} -{% tab 'Scala 3' %} ~~~ scala alice match case person: Person => person.name ~~~ -{% endtab %} -{% endtabs %} 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, * 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)). -{% tabs case_class_compat_2 class=tabs-scala-version %} -{% tab 'Scala 2' %} -~~~ scala -case class Person private (name: String, age: Int, address: Option[String]) { - ... - // Add back the former primary constructor signature - private[Person] def this(name: String, age: Int) = this(name, age, None) - def withAddress(address: Option[String]) = copy(address = address) -} -~~~ -{% endtab %} -{% tab 'Scala 3' %} -~~~ scala +{% tabs case_class_compat_2 %} +{% tab 'Scala 3 Only' %} +```scala case class Person private (name: String, age: Int, address: Option[String]): ... // Add back the former primary constructor signature private[Person] def this(name: String, age: Int) = this(name, age, None) def withAddress(address: Option[String]) = copy(address = address) -~~~ +``` {% endtab %} {% endtabs %} @@ -286,16 +243,12 @@ The original users can use the case class `Person` as before, all the methods th The new field `address` can be used as follows: -{% tabs case_class_compat_3 %} -{% tab 'Scala 2 and 3' %} ~~~ scala // The public constructor sets the address to None by default. // To set the address, we call withAddress: val bob = Person("Bob", 21).withAddress(Some("Atlantic ocean")) println(bob.address) ~~~ -{% endtab %} -{% endtabs %} 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. From 3ab5b4ba752e4575aedeb306e65a2dee7ffb57a1 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Fri, 13 Jan 2023 09:54:44 +0100 Subject: [PATCH 25/26] Final adjustments --- ...inary-compatibility-for-library-authors.md | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index 974eed4248..d3f5da030a 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -194,6 +194,7 @@ case class Person private (name: String, age: Int): // Create withXxx methods for every field, implemented by using the 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) def apply(name: String, age: Int) = new Person(name, age) @@ -204,51 +205,85 @@ object Person: {% endtabs %} This class can be published in a library and used as follows: +{% tabs case_class_compat_2 %} +{% tab 'Scala 2 and 3' %} ~~~ scala // Create a new instance val alice = Person("Alice", 42) // Transform an instance println(alice.withAge(alice.age + 1)) // Person(Alice, 43) ~~~ +{% endtab %} +{% endtabs %} 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: +{% tabs case_class_compat_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} +~~~ scala +alice match { + case person: Person => person.name +} +~~~ +{% endtab %} +{% tab 'Scala 3' %} ~~~ scala alice match case person: Person => person.name ~~~ +{% endtab %} +{% endtabs %} + 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, - * 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)). + * 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_2 %} +{% tabs case_class_compat_4 %} {% tab 'Scala 3 Only' %} ```scala case class Person private (name: String, age: Int, address: Option[String]): ... - // Add back the former primary constructor signature - private[Person] def this(name: String, age: Int) = this(name, age, None) def withAddress(address: Option[String]) = copy(address = address) ``` {% endtab %} {% endtabs %} -> 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: +And, in your build definition: + +{% tabs case_class_compat_5 %} +{% tab 'sbt' %} +~~~ scala +import com.typesafe.tools.mima.core._ +mimaBinaryIssueFilters += ProblemFilters.exclude[DirectMissingMethodProblem]("Person.this") +~~~ +{% endtab %} +{% endtabs %} + +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”. + +> Note that an alternative solution, instead of adding a MiMa exclusion filter, consists of adding back the previous +> constructor signatures as secondary constructors: > ~~~ scala -> import com.typesafe.tools.mima.core._ -> mimaBinaryIssueFilters += ProblemFilters.exclude[DirectMissingMethodProblem]("Person.this") +> case class Person private (name: String, age: Int, address: Option[String]): +> ... +> // Add back the former primary constructor signature +> private[Person] def this(name: String, age: Int) = this(name, age, None) > ~~~ -> 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”. + 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. The new field `address` can be used as follows: +{% tabs case_class_compat_6 %} +{% tab 'Scala 2 and 3' %} ~~~ scala // The public constructor sets the address to None by default. // To set the address, we call withAddress: val bob = Person("Bob", 21).withAddress(Some("Atlantic ocean")) println(bob.address) ~~~ +{% endtab %} +{% endtabs %} 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. From a818702f03522edffb1de850611f9d06195da8e4 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Fri, 13 Jan 2023 10:16:16 +0100 Subject: [PATCH 26/26] Actual final adjustments --- ...inary-compatibility-for-library-authors.md | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/_overviews/tutorials/binary-compatibility-for-library-authors.md b/_overviews/tutorials/binary-compatibility-for-library-authors.md index d3f5da030a..47261847d6 100644 --- a/_overviews/tutorials/binary-compatibility-for-library-authors.md +++ b/_overviews/tutorials/binary-compatibility-for-library-authors.md @@ -178,7 +178,7 @@ 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 private the `copy` method of the class) + * 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) * 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) @@ -197,7 +197,7 @@ case class Person private (name: String, age: Int): object Person: // Create a public constructor (which uses the primary constructor) - def apply(name: String, age: Int) = new Person(name, age) + def apply(name: String, age: Int): Person = new Person(name, age) // Make the extractor private private def unapply(p: Person) = p ``` @@ -236,6 +236,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, * 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 %} @@ -244,6 +245,10 @@ 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]): ... 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 %} {% endtabs %} @@ -287,6 +292,22 @@ println(bob.address) 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. +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' %} +~~~ 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 %} +{% endtabs %} + ## Versioning Scheme - Communicating compatibility breakages Library authors use versioning schemes to communicate compatibility guarantees between library releases to their users. Versioning schemes like [Semantic Versioning](https://semver.org/) (SemVer) allow