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

Enum families and overlapping enums #296

Closed
joroKr21 opened this issue Nov 6, 2020 · 5 comments
Closed

Enum families and overlapping enums #296

joroKr21 opened this issue Nov 6, 2020 · 5 comments

Comments

@joroKr21
Copy link

joroKr21 commented Nov 6, 2020

This works:

sealed trait Foo extends EnumEntry
object Foo extends Enum[Foo] {
  val values = findValues

  case object Foo1 extends Foo
  case object Foo2 extends Foo

  sealed trait Bar extends Foo
  object Bar extends Enum[Bar] {
    val values = findValues
    case object Bar1 extends Bar
    case object Bar2 extends Bar
  }

  sealed trait Qux extends Foo
  object Qux extends Enum[Qux] {
    val values = findValues
    case object Qux1 extends Qux
    case object Qux2 extends Qux
  }
}

What I want:

sealed trait Foo extends EnumEntry
object Foo extends Enum[Foo] {
  val values = findValues

  case object Foo1 extends Foo
  case object Foo2 extends Foo

  case object Bar1 extends Bar
  case object Bar2 extends Bar

  case object Qux1 extends Qux
  case object Qux2 extends Qux

  case object BarQux1 extends Bar with Qux
  case object BarQux2 extends Bar with Qux

  sealed trait Bar extends Foo
  object Bar extends Enum[Bar] {
    val values = findValues
  }

  sealed trait Qux extends Foo
  object Qux extends Enum[Qux] {
    val values = findValues
  }
}

Now that compiles, but at runtime Bar.values and Qux.values are empty!

Work around with Shapeless:

trait EnumValues[A] {
  def find: IndexedSeq[A]
}

object EnumValues {
  def apply[A : EnumValues]: EnumValues[A] = implicitly
  def find[A : EnumValues]: IndexedSeq[A]  = apply[A].find

  @nowarn("cat=unused")
  implicit def instance[A, C <: Coproduct, V <: HList](implicit
      generic: Generic.Aux[A, C],
      witnesses: LiftAll.Aux[Witness.Aux, C, V],
      toArraySeq: ToTraversable.Aux[V, ArraySeq, Witness.Lt[A]]
  ): EnumValues[A] = new EnumValues[A] {
    val find = toArraySeq(witnesses.instances).map(_.value)
  }
}
@lloydmeta
Copy link
Owner

The current working behaviour, search within the scope of the Enum immediately enveloping its callsite, is what enumeratum is meant to do; it does not try to search outside as you seem to want. It does this for a number of reasons, including being able to guarantee declaration-order-ordering in the sequence and bugs in other options like knownDirectSubclasses https://github.com/isomarcte/scala-compiler-knownSubclasses-bug.

If indeed, you require looking outside the scope of the enclosing Enum, you'll need to use Shapeless :)

@joroKr21
Copy link
Author

joroKr21 commented Nov 7, 2020

I'm fine with saying this is working as intended. But note that the case objects in my example are still nested in an enclosing object. It's not impossible to find them without using knownDirectSubclasses and still preserving all the properties you mentioned. findValues would just have to look further up instead of only the immediately enclosing object.

Other than that should there be a warning or something when findValues doesn't find any values?
The most confusing part is that it compiles, but fails at runtime.

@joroKr21
Copy link
Author

joroKr21 commented Nov 7, 2020

Another option would be to expose a findValuesOf[A] macro so that I could call it within Foo. findValues has the type hardcoded as Foo so I can't use it for that and defining my own macro would need another project.

@lloydmeta
Copy link
Owner

I'm fine with saying this is working as intended. But note that the case objects in my example are still nested in an enclosing object.

In your original post, you stated that your issue was that

Bar.values and Qux.values are empty`

Your Bar.values is declared as

  sealed trait Bar extends Foo
  object Bar extends Enum[Bar] {
    val values = findValues
  }

There are no Bars inside the scope of the immediately enveloping Enum[Bar].

It's not impossible to find them without using knownDirectSubclasses and still preserving all the properties you mentioned

Last I checked Shapeless conjures its Generic and friends eventually using knownDirectSubclasses anyways, then sorts them (milessabin/shapeless#892).

In any case, you can get what you want by doing something like this:

import enumeratum._

sealed trait Foo extends EnumEntry
object Foo extends Enum[Foo] {
  val values = findValues

  case object Foo1 extends Foo
  case object Foo2 extends Foo

  case object Bar1 extends Bar
  case object Bar2 extends Bar

  case object Qux1 extends Qux
  case object Qux2 extends Qux

  case object BarQux1 extends Bar with Qux
  case object BarQux2 extends Bar with Qux

  sealed trait Bar extends Foo
  object Bar extends Enum[Bar] {
    val values = Foo.values.collect { case e: Bar => e }
  }

  sealed trait Qux extends Foo
  object Qux extends Enum[Qux] {
    val values = Foo.values.collect { case e: Qux => e }
  }
}

println(Foo.values)
println(Foo.Bar.values)
println(Foo.Qux.values)

Scastie: https://scastie.scala-lang.org/Adc4z1d3TXCh3UOhz4CM1A

@joroKr21
Copy link
Author

joroKr21 commented Nov 7, 2020

That's a really nice way to make it work and no need for shapeless. Thank you 👍
Closing this now. But WDYT about a compiler warning when findValues is empty?

@joroKr21 joroKr21 closed this as completed Nov 7, 2020
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

2 participants