-
-
Notifications
You must be signed in to change notification settings - Fork 285
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
(Sealed) OneOf without Empty case #633
Comments
One assumption that ScalaPB makes is that every message has a |
we're now talking about solution btw, would solution |
The title of this issue says "(Sealed) ..." but the rest of the ticket text is about non-sealed. To reduce confusion, can you edit the title and description to match? Option 2 would require Option 1 is more doable, but like you said it needs to be provided in addition to the existing way, and it would come with additional maintenance cost and code complexity for the generator. Can you explain the motivation for this feature request? |
In my example I've used only non-sealed, but my suggestion applies to both sealed and non-sealed (both generate Maybe I should emphasize the motivation to dispel possible misunderstanding: That's why I don't want to use Is that an understandable use-case? I suspect I'm not the only one who would like to use it that way. |
@sideeffffect thanks for explaining the motivation. One difference between sealed and non-sealed: If we are going to wrap sealed oneofs in an I wonder what do you think about the following option which I believe doesn't break compatibility. Example given for sealed oneofs (but I believe can similarly work for normal oneofs as well): message MyADT {
oneof sealed_value {
TypeA a = 1;
TypeB b = 1;
}
} generates: object MyADT {
sealed trait NonEmpty extends MyADT
}
sealed trait MyADT {
def asOption: Option[MyADT.NonEmpty] = this match {
case n: MyADT.NonEmpty => Some(n)
case _ => None
}
}
case class TypeA(..) extends MyADT with MyADT.NonEmpty
case class TypeB(..) extends MyADT with MyADT.NonEmpty
case object Empty extends MyADT The difference from the current generated code is that we add a sealed trait The advantage is that it's an incremental improvement rather than introducing a new flavor of generated code. The downside is that we have two sealed traits around. |
Hmm, that's a very interesting proposal! I've remixed it a bit. What do you think? Would this also be viable? object MyADT {
sealed trait Nullable {
def asOption: Option[MyADT] = this match {
case n: MyADT => Some(n)
case _ => None
}
}
object Nullable {
case object Empty extends MyADT.Nullable
}
}
sealed trait MyADT extends MyADT.Nullable
case class TypeA(..) extends MyADT
case class TypeB(..) extends MyADT |
In my proposal? I hope not :) |
Your proposal makes sense but it breaks source compatibility since |
oh, I see your point |
This commits add a |
thank you @thesamet ! 🙏 |
@thesamet I think the proposal by @sideeffffect makes a bit more sense than the |
@ahjohannessen : @sideeffffect proposed three solutions: (1) wrapping in |
I would prefer (3) since it separates empty from |
@ahjohannessen Got it. I like that proposal too. I am fine with a breaking change around |
@thesamet Awesome :) Can the same thing be done for enum? |
@ahjohannessen Yes. I'd like enums and sealed oneofs to have a similar API as much as possible. Although I still want to collect more feedback for this proposal. In the proposed change, fields of type The term There will be a short-lived file-level (or package-level) option for the migration that would generate the existing form of the code. /cc @olafurpg for feedback |
@thesamet (a) appeals most to me, but I have no strong opinion about it. |
It may be just me, but the part that I find counter-intuitive about the proposed code layout is that The code implies |
@thesamet That makes sense, perhaps (b) is more intuitive |
I added a new style of sealed oneof that can be invoked by naming it
This would result in a structure like this:
This will be in 0.10.0-M1 which I'll release shortly. |
Released 0.10.0-M1, would appreciate feedback about this new sealed value style. I intend to keep both styles for the time being. |
@thesamet That looks great :) I suppose |
@thesamet This is an interesting approach. I like using the If I understand correctly, field references to sealed values still have the message Expr {
oneof sealed_value_or_empty {
Lit lit = 1;
Add add = 2;
}
}
message Add {
Expr a = 1;
Expr b = 1;
}
// current behavior
case class Add(a: ExprOrEmpty, b: ExprOrEmpty)
// desired behavior
case class Add(a: Expr, b: Expr) |
@ahjohannessen Yes, should be harmless. I think this came from some code sharing between the styles, in both of them the extend the empty and non-empty traits. @olafurpg Thanks for checking it out! My understanding that this should satisfy the request. In the original post, option (1) was calling to use If we'd like field reference to be of type |
Closing the issue as we have introduced here a new style of sealed value ( |
Hello @thesamet, @ahjohannessen and @olafurpg I think it's still preferable to express nullability explicitly via standard Scala So the question is, would something like this be possible? The trick here would be controlling this proposed style of code generation with the Normal message ContainingMyOneof {
oneof value_option {
HelloRequest request = 1;
HelloReply reply = 2;
}
}
message HelloMsg {
ContainingMyOneof hello = 1;
} final case class ContainingMyOneof(value: Option[….ContainingMyOneof.Value])
…
object ContainingMyOneof {
sealed trait Value …
final case class Request(value: ….HelloRequest) extends ….ContainingMyOneof.Value
final case class Reply(value: ….HelloReply) extends ….ContainingMyOneof.Value
}
…
final case class HelloMsg(hello: Option[ContainingMyOneof]) The special message ContainingMyOneof {
oneof sealed_value_option {
HelloRequest request = 1;
HelloReply reply = 2;
}
}
message HelloMsg {
ContainingMyOneof hello = 1;
} sealed trait ContainingMyOneof …
final case class Request(value: ….HelloRequest) extends ….ContainingMyOneof
final case class Reply(value: ….HelloReply) extends ….ContainingMyOneof
…
final case class HelloMsg(hello: Option[ContainingMyOneof]) What do you guys think? |
I'll look into |
having |
then:
|
wonderful!
do you think it would be desirable to have it as a flat |
@sideeffffect This is the most precise representation given that each |
@thesamet that's a very good point! 👌 One outstanding question that I have is what can we do about non-sealed oneofs? (#633 (comment)) Something like this, for example: message ContainingMyOneof {
oneof value_option {
HelloRequest request = 1;
HelloReply reply = 2;
}
}
message HelloMsg {
ContainingMyOneof hello = 1;
} final case class ContainingMyOneof(value: Option[….ContainingMyOneof.Value])
…
object ContainingMyOneof {
sealed trait Value …
final case class Request(value: ….HelloRequest) extends ….ContainingMyOneof.Value
final case class Reply(value: ….HelloReply) extends ….ContainingMyOneof.Value
}
…
final case class HelloMsg(hello: Option[ContainingMyOneof]) |
A single message can contain a number of oneofs. Are you suggesting to change the behavior for when there is an |
I was concerned with non- |
0.10.0-M2 was released today. |
Currently, for
one_of
(bothsealed_value
or normalone_of
s) generates not just the normal described cases, but also an extraEmpty
value. E.g.generates
We would need ScapaPB not to generate these extra
Empty
case objects.I see at lease two ways how to do that:
ContainingMyOneof#nonsealedValue
would not beContainingMyOneof.NonsealedValue
, butOption[ContainingMyOneof.NonsealedValue]
. That would be the most regular approach, ScalaPB encodes other fields this way too. The generated code would look like:Empty
input. The generated code would look like:Both of these solutions would be significant changes, so I presume they could be non-default, hidden behind some king of option/setting.
I would contribute the code to do that, but wanted to discuss this with ScalaPB maintainers before writing code.
relevant tickets/PRs:
#455
#526
#578
The text was updated successfully, but these errors were encountered: