enumeratum
enumeratum copied to clipboard
[Scala 3] Type-parametrized enums with intermediate hierarchies not picked by findValues
I found the behavior below during migration to Scala 3 and I couldn't find a workaround unfortunately (hence it's blocking the migration).
import enumeratum.*
sealed trait Foo2[T] extends EnumEntry with Serializable
object Foo2 extends Enum[Foo2[Unit]] {
sealed trait Bar[T] extends Foo2[T]
case object A extends Bar[Unit]
case object B extends Foo2[Unit]
lazy val values: IndexedSeq[Foo2[Unit]] = findValues
}
println(Foo2.values)
// Vector(B)
Scastie: https://scastie.scala-lang.org/w9VdeWnJTH2O1jJwAKunRQ
So we define an intermediate node in the hierarchy (Bar) and its subtypes are not found by the macros. This was working fine in scala 2. (Scastie)
It might sound a bit artificial, but we have an essential and quite complex piece of domain modeling that looks like follows (simplified)
sealed trait Account[A] extends EnumEntry
object Account {
sealed trait Asset[A] extends Account[A]
sealed trait Liability[A] extends Account[A]
}
object UserAccounts extends Enum[Account[User]] { ... }
object CompanyAccounts extends Enum[Account[Company]] { ... }
I lack the required macro expertise, but I think I pinpointed the issue. Here we check if the child is of an entry type (Foo2[Unit] in my example). Obviously Bar[T] is not, hence its subtypes are not being considered.
Unfortunately I don't know yet what a proper way to fix this is.
Yeah, I'm not actually sure I understand what is missing in Scala 3, and why #426 is needed (and more importantly why it is not something that would poke a hole through type reasoning).
https://scastie.scala-lang.org/tAL31BP3TASif48PIzohgQ
import enumeratum.*
sealed trait Foo2[T] extends EnumEntry with Serializable
sealed trait Bar[T] extends Foo2[T]
object Foo2 extends Enum[Foo2[?]] {
case object A extends Bar[Int]
case object B extends Foo2[Unit]
lazy val values: IndexedSeq[Foo2[?]] = findValues
}
println(Foo2.values)
// Vector(A, B)
There's an alternative where you fix the parent type param of the subclass (eg to Unit) that works as well.
To me it indicates there're some workaround here, which makes me reluctant to rush this.
Hmm, but that's a significantly different scenario. In the example from the ending of the first post, you can see that the goal is to define two specialized enums. We have two sets of accounts, that are disjoint and have different types. The goal is to have typesafe representations and not be able to ever pass Acount[User] when Account[Operator] is expected (and the other way around). It's similar to the Vet example you formed on https://github.com/lloydmeta/enumeratum/pull/426 . Vet[Dog] is different from Vet[Cat] and we want to define to separate sets of those Vets.
The example was built off your first one in this issue.
Feel free to poke around similarly for your use case; there may be adjustments but my point is the initial premise that hierarchies with type params don't work ... doesn't work. It does, but maybe not in your exact case.
That explains why this is the first time this has been raised, and reinforces why I won't be rushing this.
there may be adjustments but my point is the initial premise that hierarchies with type params don't work ... doesn't work.
Hmm, yes, they seem to work if we... remove type params by filling them with wildcards. Which effectively removes any value of type params. 😅 So let's maybe agree to disagree on this one.
In the meantime I realized (a bit late) I don't have to fork the full enumeratum, just use a custom macro to find values. Which is a much more acceptable approach. This will remove any time pressure from the issue.
Thanks a lot for all the time and energy you put into helping here, I really appreciate this. From my side, I will try to wrap up the work under https://github.com/lloydmeta/enumeratum/pull/426 (add the tests we discussed) and lets maybe see if there are other people who hit this problem or can contribute to the discussion :)
Hmm, yes, they seem to work if we... remove type params by filling them with wildcards. Which effectively removes any value of type params. 😅 So let's maybe agree to disagree on this one.
Wildcards in type params removing any value from type params, is an interesting take where indeed I think we will need to agree to disagree.
FWIW, my suggesting for you to feel free to poke around with adjustments was to see if you can find something else like this that might also work for your use case.
import enumeratum.*
sealed trait Foo2[T] extends EnumEntry with Serializable
sealed trait Bar[T] extends Foo2[Unit]
object Foo2 extends Enum[Foo2[Unit]] {
case object A extends Bar[Unit]
case object B extends Foo2[Unit]
lazy val values: IndexedSeq[Foo2[Unit]] = findValues
}
// Vector(A, B)
println(Foo2.values)
or even this
import enumeratum.*
sealed trait Foo2[T] extends EnumEntry with Serializable
sealed trait Bar extends Foo2[Unit]
object Foo2 extends Enum[Foo2[Unit]] {
case object A extends Bar
case object B extends Foo2[Unit]
lazy val values: IndexedSeq[Foo2[Unit]] = findValues
}
// Vector(A, B)
println(Foo2.values)
Both are enums with intermediate hierarchies involving type params. I'm not going to spend more time looking for more that could work for your usecase and, given the multiple ways illustrated in this issue alone do work, the lack of one that caters to you likely won't justify rushing into a solution w/o rigorous tests and reasoning for #426 (and as you said, you can have a custom macro until then).
Wildcards in type params removing any value from type params, is an interesting take where indeed I think we will need to agree to disagree.
Sorry, I might have butchered the nuance here a bit. I probably should have started with a less artificial example from the beginning. The problem for me is not technical in nature but spawns directly from a domain modelling perspective. Let’s take a bit less abstract example
enum Animal {
case Dog, Cat
}
sealed trait Vet[T <: Animal] extends EnumEntry
sealed trait VetSurgeon[T <: Animal] extends Vet[T]
object CatClinic extends Enum[Vet[Cat]] {
object Jon extends Vet[Cat]
object Mark extends VetSurgeon[Cat]
lazy val values: IndexedSeq[Vet[Cat]] = findValues
}
So what we are saying here is that
Vetcan treat some concrete type ofAnimalVetSurgeoncan operate on some concrete type of Animal, and its also a generalVetfor this typeCatClinicis a collection ofVets who can treatCats
Now why the workarounds are not good enough here is because they convey drastically different meanings.
object CatClinic extends Enum[Vet[?]]- a cat clinic is a collection ofVets that can treat "something" - we loseCatsepcialization entirelysealed trait Surgeon[T] extends Vet[Cat]- Surgeon can operate on some type of animal and its cat-specialized vet - makes no sense from domain perspectivesealed trait Surgeon extends Vet[Cat]- Surgeon is a cat vet that has no specialization
All of those might work in some domains but are not a solution to the general modelling problem. I feel like whatever workarounds we could find, they will address the technical problem to some extent but will not solve the root one.
I'm not going to spend more time looking for more that could work for your usecase
Totally understood, I'm already grateful for the time you spent on it already.
My point is I think the specific way of modelling could still be accommodated for , though one could of course come up with creative reasons or preferences for why it won't work, for instance via a CatSurgeon extends Vet[Cat] (or extends VetSurgeon[Cat] with Vet[Cat]). One just needs some flexibility.
If we want to have a more general fix to accommodate this specific way of modelling with intermediate hierarchy + type params where there is a technical problem (again, not saying this problem does not exist), then yes, my preference is that the fix introduces no behavioural surprises in general and comes with thorough reasoning and tests.