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

Conflict with Scala 3 enums #357

Open
ybasket opened this issue Feb 2, 2023 · 5 comments
Open

Conflict with Scala 3 enums #357

ybasket opened this issue Feb 2, 2023 · 5 comments

Comments

@ybasket
Copy link
Contributor

ybasket commented Feb 2, 2023

First of all, very cool that enumeratum made it to Scala 3, thank you to all people involved!

A coworker of mine found an issue though that seems worth fixing – you can't use enumeratum on Scala 3 enums due to a naming conflict:

import enumeratum.*
import enumeratum.EnumEntry.Lowercase

enum TShirt extends EnumEntry with Lowercase {
  case Black
  case Colorful
}

object TShirt extends Enum[TShirt] {
  override def values: IndexedSeq[TShirt] = findValues
}

results in an error:

Double definition:
def values: Array[Playground.TShirt] in object TShirt at line 25 and
override def values: IndexedSeq[Playground.TShirt] in object TShirt at line 29
have the same type after erasure.

Consider adding a @targetName annotation to one of the conflicting definitions
for disambiguation.

Removing values also doesn't work as Scala 3 generates a values method that is backed by an Array (which is no IndexedSeq).

Now, one could legitimately question whether you need to mix both, but I think it would be nice if that was possible. As the compiler suggests, a @targetName (which would rename enumeratum's values on byte code level) could be enough to fix it. Or are there are good arguments on why interop should be forbidden and people forced to use the more verbose sealed trait syntax? I couldn't find anything on the PR for Scala 3 support.

There's a workaround involving a bit of boilerplate (I didn't test it 100% yet though, it may have issues):

enum TShirt extends EnumEntry with Lowercase {
  case Black
  case Colorful
}

object TShirtHelper extends Enum[TShirt] {
  override def values: IndexedSeq[TShirt] = TShirt.values.toIndexedSeq
}

object TShirt {
  export TShirtHelper.{values => _, *}
}

Scastie for demonstration: https://scastie.scala-lang.org/kJLNLIaRQPWtqjgwDs4B1g

@lloydmeta
Copy link
Owner

Nice, thanks for bringing this up.

I don't have a hard stance about not making the two compatible, but I do have to wonder about the use case of mixing Scala 3 enums with enumeratum (fwiw I think this wouldn't have worked with Scala 2 either?).

Once we understand that, then we can better discuss whether this is something worth putting in the effort to debug and fix.

@ybasket
Copy link
Contributor Author

ybasket commented Feb 3, 2023

I don't have a hard stance about not making the two compatible, but I do have to wonder about the use case of mixing Scala 3 enums with enumeratum (fwiw I think this wouldn't have worked with Scala 2 either?).

And I don't have a hard stance on that it should be possible to mix both, but I see some advantages:

  • Clearer and shorter syntax than sealed trait
  • Scala newcomers are now learning Scala 3 with enum, so it can help the adoption of this library to support it
  • Profiting from enumeratum's large collection of integrations with common libraries while using enum

If there's no good way of integrating, I would at least suggest to add a note in the docs as the error can be a bit puzzling for people not familiar with Scala compilation internals.

@lloydmeta
Copy link
Owner

lloydmeta commented Feb 3, 2023

All valid points.

I think the biggest barrier is, as you found, the values: Array[_] that Scala 3 emits : I understand it's probably for Java compat, but I'd rather not have Enumeratum move to that; besides being a bit low-level, emitting a new array is necessary in order to be "safe" (otherwise callers can mutate the thing!).

Not exactly sure where that leaves us, but I'll leave this issue open to see if someone interested (are you?) can take up the challenge of integrating the two.

@ybasket
Copy link
Contributor Author

ybasket commented Feb 3, 2023

Yeah, I'm surprised they didn't make it an IArray (Scala 3's immutable wrapper type for arrays) at least.

I would be interested in trying to fix this in general (experimented a bit yesterday), but my OSS bucket list is considerably long already, so I don't expect to pick up this any time soon. So whoever reads this, feel invited to pick it up.

@baldram
Copy link

baldram commented Jan 23, 2024

I don't have a hard stance about not making the two compatible, but I do have to wonder about the use case of mixing Scala 3 enums with enumeratum

There's really a use-case. It helps get rid of the need to implement custom codecs every time.

Like, when we use enums in data models, for instance, for JSON responses from some API, or in databases, Enumeratum is super handy. We can have friendly names for each option, but at the same time, we can define keys that match with an external data source. It'll be easier to get what I mean with a code example.

enum TransportMode(id: String): // I need Enumeratum here to make this work (automatically)
  case Truck    extends TransportMode("TRK")
  case Vessel   extends TransportMode("SHP")
  case Airplane extends TransportMode("AIR")
  case Rail     extends TransportMode("TRN")

Thanks to Enumeratum, I can go with StringEnum and deserialize data using the value in id, while still using friendly names in my code. So, I don't have to create an enum with weird values like below and use them in my code. I just want to use them for mapping data.

enum TransportMode:
  case TRK, SHP, AIR, TRN // unfriendly names that also don't fit the code conventions 

And yeah, without Enumeratum, I could achieve the goal by implementing a custom codec. But the cool part is that with Enumeratum, I just elegantly define this mapping, and that's it. No need to write any boilerplate code for a custom codec, or access the id manually in a mapper.

Another use case is when the external source uses screaming case. That's where I'd use with Lowercase as in the example from the first comment. The external system would send in JSON values like BLACK, COLOURFUL, but in my code, I can stick to my style guide and have Black and Colourful.

Continuing from the previous explanation and summarizing the motivation:

After switching to Scala 3, I want to implement my enums slickly using the enum syntax, instead of mixing up conventions. Like, not sometimes using a sealed trait with the whole Scala 2 style (where I want to use Enumeratum), and other times enum. I just want to have consistent conventions throughout the project and always use enum.

I hope the above motivation is convincing enough that Enumeratum is still needed, even though Scala 3 has enum now. It's about making that enum be on steroids.

I don't expect to pick up this any time soon. So whoever reads this, feel invited to pick it up.

Bumping this, need help.

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

No branches or pull requests

3 participants