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

Document the pattern for evolving case classes in a compatible manner #2662

Merged
merged 26 commits into from
Jan 13, 2023

Conversation

sideeffffect
Copy link
Contributor

@sideeffffect sideeffffect commented Dec 21, 2022

Documenting the patter coming from SIP-50 - Struct Classes scala/improvement-proposals#50 (comment)
English is not my first language, so I'd be very grateful for suggestions on how to improve the wording, etc.

Rendered:
https://github.com/sideeffffect/docs.scala-lang/blob/patch-1/_overviews/tutorials/binary-compatibility-for-library-authors.md#evolving-code-without-breaking-binary-compatibility

@sideeffffect
Copy link
Contributor Author

Also, do we have some part(s) of the documentation where we discuss the concept of "binary compatibility"? It would be desirable from them to this newly created section.

@sjrd
Copy link
Member

sjrd commented Dec 21, 2022

https://docs.scala-lang.org/overviews/core/binary-compatibility-for-library-authors.html

@bjornregnell
Copy link
Contributor

I think it should be explained that the ability use pattern matching is lost if no unapply?

@sjrd
Copy link
Member

sjrd commented Dec 21, 2022

IMO this whole section belongs the guide on compatibility for library authors. Application authors never need to care about this.

@kubukoz
Copy link
Contributor

kubukoz commented Dec 21, 2022

nitpick: PR title has a typo (patter -> pattern)


object Person:
...
def apply(name: String, age: Int, address: String) = new Person(name, age, address)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't a binary compatible change, right? You're removing the method apply that takes 2 parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, you're not removing anything. You're only adding new stuff. That's the point.

Unless I missed something 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh sorry, I assumed this was replacing the apply present before. Of course you're right

@sideeffffect sideeffffect changed the title Document the patter for evolving case classes in a compatible manner Document the pattern for evolving case classes in a compatible manner Dec 21, 2022
@sideeffffect
Copy link
Contributor Author

Thanks for the feedback, I've reworked it.

@sideeffffect sideeffffect requested a review from kubukoz December 21, 2022 22:34
Copy link
Member

@Philippus Philippus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a note on the "deprecated" annotation?


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"would break its users" sounds a bit strange, don't have an alternative yet though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"would brake usage" or "would break compatible usage" or similar perhaps?


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)
Copy link
Contributor

@bjornregnell bjornregnell Dec 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, usage of instances in patterns is possible e.g. typed patterns such as
case p: Person =>
so it is "only" constructor patterns and extractor patterns that are not possible, if I'm correct?

So case Person(n,a) => would not work as unapply is private, but other kind of patterns work. So I think this should be clarified as the reader might not know about all the unapply mechanics under the hood...

The terminology of all the 15, or so, different kinds of patterns is available here: https://scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: I guess extractor patterns could be made to work if the library author provides custom extractors?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, I think it would be good to explain the implications on pattern matching in more detail...

@julienrf julienrf self-assigned this Dec 22, 2022
@sideeffffect sideeffffect requested review from kubukoz, Philippus and bjornregnell and removed request for kubukoz and Philippus December 22, 2022 12:37
~~~ 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"))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@julienrf Thanks for you suggestions. I've applied them all. But I think this part is worth a discussion.

Shouldn't we recommend readers to also include the apply methods in the companion object? Because

Person("Bob", 21).withAddress(Some("Atlantic ocean"))

looks a bit clumsy. This looks much better

Person("Bob", 21, "Atlantic ocean")

I know, we save on some boilerplate in the class definition. But we're just pushing the boilerplate onto the users of the class. Is that the right choice to make?

Ideally we'd have our cake and eat it too, but I don't know if it's possible without a new language level feature...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think the pattern I suggested is clumsy. It is similar to the “builder” pattern we often see around.

Anyway, there are probably several possible variations. The one I suggest here provides a public-facing API that (I think) is simpler because it contains fewer overloads of the apply methods.

Copy link
Contributor

@bjornregnell bjornregnell Jan 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, documentation space is not scarce :) so I suggest to explain both possibilities here: withX or overloaded apply constructors. It's up to the library author. And if the library user is "refused" a more concise constructor then it's just a self-made extension away...

@sideeffffect
Copy link
Contributor Author

sideeffffect commented Jan 1, 2023

There are parts of this pattern which are pretty repetitive and very regular:

  • add a new constructor, private to the companion object, with the original fields (necessary, unless we want to deal with MiMa)
  • add an apply factory method to the companion (necessary, unless we want to burden the class users with boilerplate)

Are these parts automatable using Macros? We would need the macro(s) to generate as many of these members as there are Options in the case class (well, only the Options that are on the right side of the case class constructor definition).

(Btw good things about this patten is that it seems to work with e.g. Circe for JSON codecs https://scastie.scala-lang.org/huP4tCstQBufYKFwXUFs4Q )

@sjrd
Copy link
Member

sjrd commented Jan 2, 2023

Anything that needs to add members visible during typechecking cannot be done with macros.

@julienrf
Copy link
Contributor

julienrf commented Jan 6, 2023

@sideeffffect do you want to rework some parts or should we merge it as it is? (I am fine with both)

@sideeffffect
Copy link
Contributor Author

I thought we want to first finish the MiMa vs "private" primary constructors debate.

https://contributors.scala-lang.org/t/private-primary-constructor-vs-mima/6050/5

But I'm not against merging this documentation before. What do you guys think?

@julienrf
Copy link
Contributor

julienrf commented Jan 6, 2023

That’s a good point.

@smarter, what do you think about Sébastien’s suggestion of emitting the constructors as “package private” constructors? If we can do that, then I guess we would not need to add the MiMa exception anymore.

@smarter
Copy link
Member

smarter commented Jan 6, 2023

That would also be a binary breaking change which I assume mima would flag unless we special-case it in mima.

@julienrf
Copy link
Contributor

julienrf commented Jan 9, 2023

I assume MiMa already treats package private definitions like private definitions. If this is not the case, that should be fixed indeed. But then MiMa would not flag changes to the class constructors if they are emitted as package private. Of course, code compiled with a new version of the compiler (emitting private case class constructors as package private constructors at the bytecode level) would be binary incompatible with code compiled with a previous compiler, so library authors would have to add a MiMa exception for that. But at least, the emitted bytecode would be more correct because it would become impossible to call the constructor from Java code.

@julienrf
Copy link
Contributor

I created scala/bug#12711 and scala/scala3#16651 to track the issue with private constructors that are still public.

We could add a note about that in the documentation, what do you think?

Co-authored-by: Julien Richard-Foy <[email protected]>
@julienrf
Copy link
Contributor

I did another pass and made some final adjustments. I think the guide is ready to go now.

Screenshot 2023-01-13 at 09-51-29 Binary Compatibility for library authors

@julienrf julienrf assigned sideeffffect and unassigned julienrf Jan 13, 2023
@bishabosha
Copy link
Member

This doesn't mention defining a new apply method in the companion (and keeping the old one) after adding the address field

@bjornregnell
Copy link
Contributor

Minor thing, fix word ordering to be "this makes the copy method private".

@julienrf
Copy link
Contributor

Thank you for the review, @bishabosha and @bjornregnell, I’ve addressed your comments in a818702

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants