lloydmeta / enumeratum

A type-safe, reflection-free, powerful enumeration implementation for Scala with exhaustive pattern match warnings and helpful integrations.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Scala 3: What should be done

lloydmeta opened this issue · comments

commented

Looking at Scala 3, the enumerations built into the language look pretty good.

The questions are

  1. What should enumeratum look like in a Scala 3 world?
    • What problems are we trying to solve? The problems with Scala 2 enums were pretty obvious, which led to the core of this lib

The first step towards Scala 3 for any real-world project is cross-compilation between Scala 2.13/Scala 3. Most of the OSS libs added Scala 3 to their cross-builds.

IMO, this will be a status quo for the next few years. Only then we'll drop Scala 2 support and migrate to the new enums.

Thus, the Enumeratum macros should be ported.

My 2c: we use Enumeratum mostly to model things that map to postgres enums, with a nice benefit of being able to have safe withName* overloads for decoding.

I'm not sure if Scala 3 enums has support for this...

commented

The first step towards Scala 3 for any real-world project is cross-compilation between Scala 2.13/Scala 3. Most of the OSS libs added Scala 3 to their cross-builds.

IMO, this will be a status quo for the next few years. Only then we'll drop Scala 2 support and migrate to the new enums.

Thus, the Enumeratum macros should be ported.

Fair point; have you checked that the macro(s) in its current form can be ported? A PR would be welcome to start concrete discussions as well.

commented

My 2c: we use Enumeratum mostly to model things that map to postgres enums, with a nice benefit of being able to have safe withName* overloads for decoding.

I'm not sure if Scala 3 enums has support for this...

I think there is a way of having custom names, based on this doc https://github.com/dotty-staging/dotty/blob/master/docs/docs/reference/enums/desugarEnums.md#translation-of-enums-with-singleton-cases

Nevermind... looks like support for custom enumLabel was added, then later removed, and I can't figure out how to get it working (Scastie attempts)

@lloydmeta I think you were looking for valueOf: scastie session

commented

@note Hmmm I think that's almost it, but how do we make the second print resolve Red from the custom name "red"?

@lloydmeta Ah, I missed the point that it's about custom names as I mostly looked at scastie session using outdated ofValue. I am not sure about support for custom names in Scala 3 as well

Thus, the Enumeratum macros should be ported.

any plan to have the macros ported?

commented

Thus, the Enumeratum macros should be ported.

any plan to have the macros ported?

The plan is:

  1. Come up with a working prototype for the macro, either on Scastie or a PR
    • In particular, the enclosingModule function is basically gone with no replacement in Scala 3 AFAIK, so we can't explore inside a "scope" for findValues
      private[enumeratum] def enclosedSubClassTreesInModule(c: Context)(
      typeSymbol: c.universe.Symbol,
      enclosingModule: c.universe.ModuleDef
      ): List[c.universe.ModuleDef] = {
      import c.universe._
      enclosingModule.impl.body.flatMap(_ match {
      case m: ModuleDef
      if m.symbol.isModule &&
      m.symbol.asModule.moduleClass.asClass.baseClasses.contains(typeSymbol) =>
      m :: enclosedSubClassTreesInModule(c)(typeSymbol, m)
      case m: ModuleDef =>
      enclosedSubClassTreesInModule(c)(typeSymbol, m)
      case _ => List.empty
      })
      }
  2. Implement the prototype.

Currently stuck on (1).

In particular, the enclosingModule function is basically gone with no replacement in Scala 3 AFAIK

There is some dotty/scala3 issue about that?

Alternatively, could we change the signature of findValues so we can pass that enclosingModule, like:

sealed abstract class Light extends EnumEntry
case object Light extends Enum[Light] {
  val values = findValues[Light]
  case object Red   extends Light
  case object Blue  extends Light
  case object Green extends Light
}

Sorry if this, does not make sense at all, because I am not very familiar with macros...

commented

In particular, the enclosingModule function is basically gone with no replacement in Scala 3 AFAIK

There is some dotty/scala3 issue about that?

Nope. I think it was a conscious decision/redesign around the Scala 3 macro system.

Alternatively, could we change the signature of findValues so we can pass that enclosingModule, like:

sealed abstract class Light extends EnumEntry
case object Light extends Enum[Light] {
  val values = findValues[Light]
  case object Red   extends Light
  case object Blue  extends Light
  case object Green extends Light
}

It's not so much a problem of having to provide the type param (the compiler can figure that out in the same way I think), just AFAICT, the functionality to explore the object scope is gone.

I think one way might be to change it into something that works on a defined scope like so, but it's not backwards-source compatible.

sealed abstract class Light extends EnumEntry
@findEnumValues // or find members at this point
case object Light extends Enum[Light] {
   case object Red   extends Light
   case object Blue  extends Light
   case object Green extends Light
}

To be clear: the only helpful next step in this issue is for someone to come forth with a working prototype.

commented

Actually it's possible to access "enclosingModule" via Symbol.spliceOwner.owner...owner.
Here is a prototype: Scastie

commented

@marq Thanks so much for that (even including a Scastie!). I'm very unfamiliar with the new Scala 3 macros (and it seems like the documentation may not be the most complete ?) so I'm having a hard time and would really appreciate the help of those who are such as yourself 🙏🏼

To provide a clean sandbox to hack around, I created a separate repo to explore porting macros to Scala 3 some more, and split it into 3 issues, one for each main macro method:

https://github.com/lloydmeta/mune/issues

Since you've done most of the work, lloydmeta/mune#1 is basically done, but I'm running into some errors on lloydmeta/mune#2 (materialising the companion object of an enum entry type); wondering you can spot anything wrong there. lloydmeta/mune#3 is also a TODO.

Hi @lloydmeta ! It seems that enumeratum still provides stuff that native scala3 enums don't, does it? Would be great to have I guess.
Are you actively working on that?

commented

Hi @lloydmeta ! It seems that enumeratum still provides stuff that native scala3 enums don't, does it? Would be great to have I guess.
Are you actively working on that?

Hey there @mauhiz , yeah, it turns out there are some things missing from the official Scala 3 enums that is supported here. I agree it would be nice to have, but I got stuck in my attempt to port the macro to Scala3 (see #300 (comment)). I'm not actively working on it and any help would be appreciated :)

commented

Thanks to @tpunder, the Scala 3 port is mostly complete.

The remaining bit is lloydmeta/mune#3, which involves porting over this bit

private[this] def findValueEntriesImpl[
ValueEntryType: c.WeakTypeTag,
ValueType: ClassTag,
ProcessedValue
](c: Context)(
processFoundValues: ValueType => ProcessedValue
): c.Expr[IndexedSeq[ValueEntryType]] = {
import c.universe._
val typeSymbol = weakTypeOf[ValueEntryType].typeSymbol
EnumMacros.validateType(c)(typeSymbol)
// Find the trees in the enclosing object that match the given ValueEntryType
val subclassTrees = EnumMacros.enclosedSubClassTrees(c)(typeSymbol)
// Find the parameters for the constructors of ValueEntryType
val valueEntryTypeConstructorsParams =
findConstructorParamsLists[ValueEntryType](c)
// Identify the value:ValueType implementations for each of the trees we found and process them if required
val treeWithVals = findValuesForSubclassTrees[ValueType, ProcessedValue](c)(
valueEntryTypeConstructorsParams,
subclassTrees,
processFoundValues
)
if (weakTypeOf[ValueEntryType] <:< c.typeOf[AllowAlias]) {
// Skip the uniqueness check
} else {
// Make sure the processed found value implementations are unique
ensureUnique[ProcessedValue](c)(treeWithVals)
}
// Finish by building our Sequence
val subclassSymbols = treeWithVals.map(_.tree.symbol)
EnumMacros.buildSeqExpr[ValueEntryType](c)(subclassSymbols)
}

I think the main tricky bit is finding how to destructure the different ways to declare value in a Scala 3 world.

@lloydmeta any update on this? Thanks

commented

There's a PR open (#349) that I've slowly been reviewing over iterations.

commented

Done.

First off, thank you.

Second, I notice that there are two separate libraries: one for scala 3 and one for scala 2. Is there any plan or desire to have a mixed library that can be used in scala 3 and in scala 2? This would be useful for large projects that use enumeratum that want to migrate to scala 3 gradually. I think the alternatives would be either migrating all at once, or depending on both libraries, and I don't know how well depending on both libraries would work out in practice (I imagine not well)

commented

Is there any plan or desire to have a mixed library that can be used in scala 3 and in scala 2

There's no plan, but it does sound desirable; do you want to give it a stab ?

Sure, I'll take a stab at it

I have something that I think would work, but there's a bug in the macro mixing functionality that is triggered by using type parameters from the class level in a scala 2 macro call. So I don't think any further progress can be made until that's fixed.

branch:
master...lucidsoftware:enumeratum:mix-scala-2-and-3-macros
scala bug:
scala/scala3#16630