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

Higher kinded type support #374

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package enumeratum.values

private[values] trait ValueEnumSpecCompat { this: ValueEnumSpec =>
// recursively referential types can't be defined inside a describe block
abstract class AbstractEnumEntry[A, Comp <: AbstractEnumEntryCompanion[A, ?]](
override val value: String
) extends StringEnumEntry
abstract class AbstractEnumEntryCompanion[A, Entry <: AbstractEnumEntry[A, ?]](entry: Entry) {
def thing: A
}

def scalaCompat = describe("Scala2 higher-kinded types") {
it("should work with higher-kinded type parameters") {
// In scala 2, `extends StringEnum[Entry[?]]` was allowed, and worked fine with findValues.
// In scala 3, `extends StringEnum[Entry[?]]` is not allowed (it fails with "unreducible application of
// higher-kinded type Entry to wildcard arguments"). The closest thing is `extends StringEnum[Entry[Any]]`,
// which would not have worked with findValues in scala 2, but has been made to work in scala 3 as the
// existential types needed to resolve the type validly have been removed.
"""
abstract class AbstractEnum[
Entry[A] <: AbstractEnumEntry[A, Companion[A]],
Companion[A] <: AbstractEnumEntryCompanion[A, Entry[A]]
] extends StringEnum[Entry[?]]

sealed abstract class Enum[A](value: String) extends AbstractEnumEntry[A, EnumCompanion[A]](value)
sealed abstract class EnumCompanion[A](entry: Enum[A]) extends AbstractEnumEntryCompanion[A, Enum[A]](entry)

object Enum extends AbstractEnum[Enum, EnumCompanion] {
case class One(thing: Int) extends EnumCompanion[Int](One)
case object One extends Enum[Int]("One")

case class Two(thing: String) extends EnumCompanion[String](Two)
case object Two extends Enum[String]("Two")

override def values: IndexedSeq[Enum[?]] = findValues
}
""" should compile
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package enumeratum.values

private[values] trait ValueEnumSpecCompat { this: ValueEnumSpec =>
// recursively referential types can't be defined inside a describe block
abstract class AbstractEnumEntry[A, Comp <: AbstractEnumEntryCompanion[A, ?]](
override val value: String
) extends StringEnumEntry
abstract class AbstractEnumEntryCompanion[A, Entry <: AbstractEnumEntry[A, ?]](entry: Entry) {
def thing: A
}

def scalaCompat = describe("Scala3 higher-kinded types") {
it("should work with higher-kinded type parameters") {
// In scala 2, `extends StringEnum[Entry[?]]` was allowed, and worked fine with findValues.
// In scala 3, `extends StringEnum[Entry[?]]` is not allowed (it fails with "unreducible application of
// higher-kinded type Entry to wildcard arguments"). The closest thing is `extends StringEnum[Entry[Any]]`,
// which would not have worked with findValues in scala 2, but has been made to work in scala 3 as the
// existential types needed to resolve the type validly have been removed.
"""
abstract class AbstractEnum[
Entry[A] <: AbstractEnumEntry[A, Companion[A]],
Companion[A] <: AbstractEnumEntryCompanion[A, Entry[A]]
] extends StringEnum[Entry[Any]]

sealed abstract class Enum[A](value: String) extends AbstractEnumEntry[A, EnumCompanion[A]](value)
sealed abstract class EnumCompanion[A](entry: Enum[A]) extends AbstractEnumEntryCompanion[A, Enum[A]](entry)

object Enum extends AbstractEnum[Enum, EnumCompanion] {
case class One(thing: Int) extends EnumCompanion[Int](One)
case object One extends Enum[Int]("One")

case class Two(thing: String) extends EnumCompanion[String](Two)
case object Two extends Enum[String]("Two")

override def values: IndexedSeq[Enum[Any]] = findValues
}
""" should compile
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import org.scalatest.matchers.should.Matchers
*
* Copyright 2016
*/
class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers {
class ValueEnumSpec
extends AnyFunSpec
with Matchers
with ValueEnumHelpers
with ValueEnumSpecCompat {

describe("basic sanity check") {
it("should have the proper values") {
Expand Down Expand Up @@ -135,6 +139,22 @@ class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers {
""" should (compile)
}

it("should compile when there is a hierarchy of sealed traits") {
"""
sealed abstract class Top(val value: Int) extends IntEnumEntry
sealed trait Middle extends Top

case object Top extends IntEnum[Top] {
case object One extends Top(1)
case object Two extends Top(2)
case object Three extends Top(3) with Middle
case object Four extends Top(4) with Middle

val values = findValues
}
""" should compile
}

it("should fail to compile when there are non literal values") {
"""
sealed abstract class ContentTypeRepeated(val value: Long, name: String) extends LongEnumEntry
Expand All @@ -152,6 +172,21 @@ class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers {
}
""" shouldNot compile
}

it("should compile when entries accept type parameters") {
"""
sealed abstract class ExampleEnumEntry[Suffix](override val value: String) extends StringEnumEntry {
def toString(suffix: Suffix): String = value + suffix.toString
}

object ExampleEnum extends StringEnum[ExampleEnumEntry[?]] {
case object Entry1 extends ExampleEnumEntry[Int]("Entry1")
case object Entry2 extends ExampleEnumEntry[String]("Entry2")

override def values: IndexedSeq[ExampleEnumEntry[?]] = findValues
}
""" should compile
}
}

describe("trying to use with improper types") {
Expand Down Expand Up @@ -209,4 +244,6 @@ class ValueEnumSpec extends AnyFunSpec with Matchers with ValueEnumHelpers {
}
}
}

scalaCompat
}
48 changes: 38 additions & 10 deletions macros/src/main/scala-3/enumeratum/ValueEnumMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ object ValueEnumMacros {
tpe: Type[A],
valueTpe: Type[ValueType]
)(using cls: ClassTag[ValueType]): Expr[IndexedSeq[A]] = {
type TakeHead[Head <: A & Singleton, Tail <: Tuple] = Head *: Tail
type SingletonHead[Head <: Singleton, Tail <: Tuple] = Head *: Tail
type OtherHead[Head, Tail <: Tuple] = Head *: Tail

type SumOf[X <: A, T <: Tuple] = Mirror.SumOf[X] {
type MirroredElemTypes = T
Expand Down Expand Up @@ -180,29 +181,43 @@ In SBT settings:
}
}

object CorrectType {
def unwrap(tpe: TypeRepr) = tpe match {
case AppliedType(tpe, _) => tpe
case _ => tpe
}
def unapply(tree: TypeTree): Boolean = unwrap(tree.tpe) <:< unwrap(repr)
}

@annotation.tailrec
def collect[T <: Tuple](
instances: List[Expr[A]],
values: Map[TypeRepr, ValueType]
)(using tupleTpe: Type[T]): Either[String, Expr[List[A]]] =
tupleTpe match {
case '[TakeHead[h, tail]] => {
case '[SingletonHead[h, tail]] => {
val htpr = TypeRepr.of[h]

(for {
vof <- Expr.summon[ValueOf[h]]
constValue <- htpr.typeSymbol.tree match {
case ClassDef(_, _, spr, _, rhs) => {
val fromCtor = spr
.collectFirst {
case Apply(Select(New(id), _), args) if id.tpe <:< repr =>
args
case classDef @ ClassDef(_, _, spr, _, rhs) => {
val treeAcc = new TreeAccumulator[Option[List[Term]]] {
def foldTree(value: Option[List[Term]], tree: Tree)(
owner: Symbol
): Option[List[Term]] = value.orElse {
tree match {
case Apply(Select(New(CorrectType()), _), args) => Some(args)
case Apply(TypeApply(Select(New(CorrectType()), _), _), args) => Some(args)
case _ => foldOverTree(None, tree)(owner)
}
}
}
treeAcc
.foldTrees(None, spr)(classDef.symbol)
.flatMap(_.lift(valueParamIndex).collect { case ConstVal(const) =>
const
})

fromCtor
.orElse(rhs.collectFirst { case ConstVal(v) => v })
.flatMap { const =>
cls.unapply(const.value)
Expand All @@ -213,7 +228,7 @@ In SBT settings:
case _ =>
Option.empty[ValueType]
}
} yield Tuple3(TypeRepr.of[h], '{ ${ vof }.value: A }, constValue)) match {
} yield Tuple3(TypeRepr.of[h], '{ ${ vof }.value.asInstanceOf[A] }, constValue)) match {
case Some((tpr, instance, value)) =>
collect[tail](instance :: instances, values + (tpr -> value))

Expand All @@ -224,6 +239,19 @@ In SBT settings:
}
}

case '[OtherHead[h, tail]] =>
Expr.summon[Mirror.SumOf[h]] match {
case Some(sum) =>
sum.asTerm.tpe.asType match {
case '[SumOf[a, t]] => collect[Tuple.Concat[t, tail]](instances, values)

case _ => Left(s"Invalid `Mirror.SumOf[${TypeRepr.of[h].show}]")
}

case None =>
Left(s"Missing `Mirror.SumOf[${TypeRepr.of[h].show}]`")
}

case '[EmptyTuple] => {
val allowAlias = repr <:< TypeRepr.of[AllowAlias]

Expand Down