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 case classes for Ember client and server configuration #7066
base: series/0.23
Are you sure you want to change the base?
Conversation
Sorry 👎 regardless of the guide, compatibly-evolving case classes is a significant burden on maintainers and (new) contributors. Have you considered contributing this to PureConfig's http4s module instead? https://github.com/pureconfig/pureconfig/tree/master/modules/http4s |
PureConfig maintainers refuse to have config case classes like this in their repository. I think this is a good fit or https://github.com/http4s/http4s It is configuration for http4s after all. And the burden isn't so great, especially compared to the huge benefits for the users. New parameters just have to be added to |
🤷
IMHO this is idealistic :) not all changes are as simple as adding a new parameter. What happens if you want to change an existing one for example? In my experience I'd be happy to see Scala get a proper |
But aren't we in that case in the same situation as with the current Builders ( So if you're not worried about adding a new field (I made special care to make it easy for the case classes in this PR), what benefit are we getting from having only Builders and staying away from case classes? I totally understand the need to keep things easy in order not to overload maintainers. What is is specifically that you're worried about? That somebody would add a new field without making the necessary changes to keep the backward compatibility? I'm hopeful we can find some way to enable this, because there's a huge demand for this and it would be sad to let it go unsatisfied. Please hear me out, maybe together we can figure this out 🙏 |
@sideeffffect could you perhaps say more about this demand? A brief search through past issues hasn't really convinced me this is the case. There's your prior work in #3081 which ultimately hit on the same issue, case classes increase maintenance burden. Also perhaps an interesting and much older take on configuration: #105 |
@valencik the reasoning behind this is that if you have a case class, you can derive from it various useful tools, like for example the Yes, that old PR #3081 had just plain case classes, which aren't backward compatible. But this PR is different, right? This one is written with compatibility in mind. My hope is that the maintenance burden wouldn't actually be that high. It's adding a new field that's tricky, and as I've written in the comment, then you "only" need to add it to |
Ah, sorry for the confusion, I understand the use case. I'm curious about the demand you mentioned. Are you seeing lots of people ask for this in other places? |
In every company I've worked with, people want/need config case classes like this and have to workaround their lack. Having "official" config case classes directly in the http4s project itself would fix the problem for oh so many people once and for good. |
No, we're not in the same situation. You can easily change a parameter type in the current builder encoding (assuming there is a mapping from the old one) but doing this for a We can argue this ad nauseam :) I am very sorry to be dismissive. But I've done this sort of The best I can offer you is that it is now easier than ever to publish your own lib with sbt-typelevel. So if you are confident the maintenance burden will be minimal then that seems like a good option :) |
I don't know that this encoding was even feasible when we designed the builders. The meaning of private in case classes and how it suppresses certain features has evolved. So let's look with fresh eyes. Unchanged:
Pros:
Cons:
In a greenfield, the case class encoding, with privates as described in that link, is compelling. (I'd want to practice some breaking changes before I was convinced.) I'm not sure I see enough demand or upside yet to justify a breaking change. |
Why is this? The only difference I spotted in the doc is the syntax. |
Because of |
Just to be clear, I'm not proposing any breaking change, let alone removing the builders. The builders are more general, and server a little bit different purpose IIUC. The config classes are designed only for data (which means equals/hashCode is well defined there). To the best of my knowledge (but I don't know everything, of course 😅 ), my current proposal should work for both Scala 2 and 3. How would |
Could you please explain this a little bit more. I don't really understand the different between case class and a Builder in this situation. Maybe even a super small example would make it clear to me. Thank you for engaging with me on this, I appreciate it. 🙏 Again, I very much value http4s maintainers' time and wouldn't want to burden them unreasonably. |
Sure. Suppose I have: case class Bar(value: String)
case class Foo(bar: Bar) And I want to change it to: case class Baz(value: String)
case class Foo(baz: Baz) How would you do that compatibly? :) I know this seems stupidly trivial. A real-world example of this kind of change is swapping a type from something that is JVM-only for an isomorphic type that exists on all three platforms. Ross offered another scenario where this sort of thing can happen.
|
I think it can be binary and source (with deprecations) compatible using the encoding in the scala-lang.org article. We can't preserve semantic compatibility, which is a new problem. Addition is easy, and just like the guide:
Removal less so:
We've changed the semantics of Here's where I get fuzzy, because I don't use Scala 3 except to fix crossbuilds: I see how we've similarly broken things derived from Mirror semantically. But the generated mirrors are not part of the ABI, right? And anything we derive with them produce code against the stable public API, no? Now we have the same kind of problems, but extended past the universal APIs, but I don't see how separate Scala 2 or 3 branches help. I don't doubt you're right, but I'm missing a piece of the puzzle. |
Hmm, I may have lost the thread on this a bit 😅 what are we aiming for here? If we are saying so long as it's source-compatible and binary-compatible, ship it, I don't care if it crashes at runtime, then yeah, I suppose this is good. If we decide we do care about such things (I think this is the "semantic compatibility" Ross is referring to?) then it gets a lot harder. Firstly, the tooling won't help us stay compatible here, and secondly, it will require mucking about with Scala version-specific APIs for derivation. In any case, to aid exploration, I created a little sandbox in armanbilge/sandbox@0ab5302. That sandbox includes:
The current demo shows that even simply adding a field to a |
This is both an illuminating and horrifying example. Thank you! When I say semantic changes, I mean things that compile, but whose behavior changes:
The first one? Thou shalt not use The second one works fine if the derivation is kept with the class. Thou shalt not derive orphan instances? Great, but where is the full list of these commandments, and who is going to make a linter out of them? Are we going to keep blaming apps one crash at a time? |
523d285
to
27edccc
Compare
Thanks for investigating this with me, I really appreciate it 🙇 Thankfully the problem with def fromProduct(p: Product): Person = p.productArity match {
case 1 =>
Person(
p.productElement(0).asInstanceOf[String]
)
case 2 =>
Person(
p.productElement(0).asInstanceOf[String],
p.productElement(1).asInstanceOf[Int]
)
} I know, it's yet another step to do when adding a new field, but I think it's still easy (albeit a bit embarrassing). Then don't have the Scala 3 problem with as shown here armanbilge/sandbox@0ab5302 btw I mentioned this
Not a problem IMHO. And removing a member isn't backward compatible regardless if you do case class or a builder, right? |
Nice job! But I think this is just the tip of the iceberg :) in armanbilge/sandbox@c361606 I demonstrated another runtime crash due to a semantic incompatibility, even after applying your |
But that's not a problem with case classes, let alone http4s. That's just a bug in Scala 3 derivation of case class Person(name: String, age: Int = -1) and { name = foo } |
Hmm, I'm not sure I agree.
|
Maybe I'm misunderstanding your post, but it's not that it requires us to encode defaults in a specific way, it just doesn't work. But picking up defaults is super important feature of PureConfig, so I hope they will fix it one way or another, PureConfig wouldn't be as useful without it. |
Hello again @armanbilge @rossabaker @valencik . Are you guys open to continuing the discussion about this? Do you think we could find a common ground about this? |
Sure, but to make it work requires encoding defaults in a specific way, that is not part of
I'm open to seeing a demo that this actually works on Scala 2 and 3. I understand that's a bit tricky at the moment since Scala 3 support is still spotty. However at the moment I'm still not convinced that we can actually evolve compatibly here without making additional assumptions or following "commandments". I think compatible evolution of case class like datatypes that support derivation still needs to be addressed at the language level. |
Would this count as the demo? armanbilge/sandbox#1
Yes, PureConfig doesn't work with default values in Scala 3. But that's a PureConfig issue, not a binary incompatibility issue -- you don't get a runtime crash. |
I wrote a comment there.
As I established in #7066 (comment), satisfying MiMa doesn't mean we can be confident that it won't crash at runtime for some user of our library. |
Replied there too 🙂
But is that really true? The code is binary compatible, there are no (potentially used) members that suddenly disappear. It's true that PureConfig for Scala 3 doesn't work with default arguments. But there is no crash. If you try parsing a config that doesn't have all the fields of the case class, Scala 3 PureConfig won't be able to parse it. But it won't crash, it will just fail parsing. That's just a bug in Scala 3 PureConfig, it's definitely not a problem of binary compatibility in http4s. Of course I agree that it would be better if Scala 3 PureConfig worked with default arguments. But that's a responsibility of the PureConfig developers. Let's suppose Scala 3 PureConfig is suddenly fixed, but somebody, could be me, create my own, buggy derivation library for something, will it also be a reason for not accepting a PR like this one? Or what if I created my own configuration library ala PureConfig that didn't suffer from this problem, would it suddenly make this PR acceptable? It seems to me that you're saying that if there is (could?) be a library, that could use the code (configuration case classes in our case) in this PR in a buggy way, we can't accept the PR. But isn't that overly strict? Is this strictness even applied to other PRs? I don't think bugs in unrelated (not a dependency) projects should hinder the http4s project. Aren't we letting the perfect to be the enemy of the good with this approach? |
The problem lies only with PureConfig's buggy implementation. There are other Scala 3 libraries that don't break with case classes and their default values and work well when trying to decode an "old" string. One example is uPickle. As a proof, I've prepared this PR: I hope that this shows that case classes (with some special care, of course) can be part of http4s and don't pose a backward compatibility risk. |
Thanks!
I don't think I agree that it is a bug. As I state in scala/docs.scala-lang#2788 (comment), the |
By "we" you mean http4s? I'm asking, because this doesn't have anything to do with http4s. This is a concern of other libraries, e.g. uPickle, right? It's not that I'm against "graduate to a language-level feature". But I don't think deficient (regardless if we call it a "bug" or not) implementations of other, non-http4s, libraries should hold http4 back. Why couldn't http4s and uPickle users enjoy nice features? Only because PureConfig doesn't do as good job as uPickle does? Users which would like to use http4s with PureConfig should take it up with PureConfig developers. http4s shouldn't (and can't) be responsible for all Scala libraries or even the language itself. |
Yes, I mean http4s. It does concern us. How do we add default values? There is no place to add them in the
To clarify, even if
Exactly :) |
The way I did it in the PR I linked above seems like a good way. It works with at least some Scala 3 libraries, e.g. uPickle.
Yes, that is true.
A way. A way which apparently doesn't work well with default arguments (at least currently). But there are libraries which apparently don't use it, or use it differently from what PureConfig does and thus work fine with default arguments. One such is uPickle. To recapitulate, case classes (with special care) don't pose risk for backward compatibility via
But in contrast to the bytecode of http4s artifacts, the 3rd party libraries http4s users use are not under http4s maintainers control. And thus, I think, shouldn't be reasons for accepting/rejecting a PR to http4s. I think I've presented the case as best as I could, I don't think I can add or do anything more. Now it's up to the @http4s maintainers to decide on this PR. |
Case classes are incredibly useful for configuration, e.g. with PureConfig. They are tricky with regards to backward compatibility. But with the way as done here, backward compatibility will be maintained.
https://docs.scala-lang.org/overviews/core/binary-compatibility-for-library-authors.html#evolving-code-without-breaking-binary-compatibility