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

Solution for generic typeclass derivation for Enums!

V-Lamp opened this issue · comments

This solves the pain of needing to extend the companion object of each and every enumeration in order to support some 3rd party library.
Why is this a problem?

  • Repetitive: extend a dozen companions if I need to add say Circe
  • Bad domain modelling: mixes the domain definition with implementation concerns
  • May not be extendable: Domain can be defined in a shared library so cannot extend it to support the fancy new library I want to use

The problem with any generic derivation that will work for all enumerations is that you need access to the companion object.
What if we add a type class to do just that?

/**
 * This can be just `Enum` if this solution is accepted!
 */
trait ImplicitEnum[T <: EnumEntry] extends Enum[T] {
  implicit val _enumCompanion: EnumCompanion[T] = EnumCompanion.make(this)
}

/**
 * A type class to get the companion object of the Enumeration implicitly
 */
trait EnumCompanion[T <: EnumEntry] {
  val companion: Enum[T]
}
object EnumCompanion {
  def apply[T <: EnumEntry : EnumCompanion]: Enum[T] = implicitly[EnumCompanion[T]].companion

  def make[T <: EnumEntry](comp: Enum[T]): EnumCompanion[T] = new EnumCompanion[T] {
    override val companion: Enum[T] = comp
  }
}

Then use like this for e.g. doobie:

  implicit def doobieEnum[T <: EnumEntry : EnumCompanion]: Meta[T] =
    Meta[String].imap(EnumCompanion[T].withNameInsensitive)(_.entryName)

With this, all enumerations in the codebase can now be used with doobie 🎉 (Assuming this implicit is available when needed)

I am sure you can see that this is a generic solution that can work for many libraries. I would argue in fact that may make the explicit support of some libraries redundant, given how easy this second code snippet is to write.

What are your thoughts?

I think this is a purely additive change, that just enables a class of solutions that weren't previously reachable. Happy to make a PR on this if it makes sense to you.

EDIT:

Here is a concrete example of the abstractions gained by this:

**
 * A typeclass to provide the desired decoding uniformly for enumerations.
 * E.g. you may want only uppercase enumerations to be parsed from the database.
 */
trait EnumDecoder[T <: EnumEntry] {

  def decodeEither(raw: String): Either[NoSuchMember[T], T]
  def decodeOption(raw: String): Option[T] = decodeEither(raw).toOption
  def decode(raw: String): T = decodeEither(raw)
    .fold(err => throw new NoSuchElementException(err.getMessage()), identity)
}
object EnumDecoder {

  def apply[T <: EnumEntry : EnumDecoder]: EnumDecoder[T] = implicitly[EnumDecoder[T]]

  trait Insensitive {
    implicit def decoder[T <: EnumEntry: EnumCompanion]: EnumDecoder[T] = of[T](_.withNameInsensitiveEither)
  }
  object Insensitive extends Insensitive

  trait Exact {
    implicit def decoder[T <: EnumEntry : EnumCompanion]: EnumDecoder[T] = of[T](_.withNameEither)
  }
  object Exact extends Exact

  def of[T <: EnumEntry : EnumCompanion](f: Enum[T] => String => Either[NoSuchMember[T], T]): EnumDecoder[T] =
    (raw: String) => f(EnumCompanion[T])(raw)
}

then the above example becomes:

  implicit def doobieEnum[T <: EnumEntry : TypeName : EnumCompanion : EnumDecoder]: Meta[T] =
    Meta[String].imap(EnumDecoder[T].decode)(_.entryName)

If the user wants to change the casing used for decoding, they just import a different decoder, e.g. import EnumDecoder.Insensitive._

commented

Maybe I'm misunderstanding, but Enum has this:

/** Finds the Enum companion object for a particular EnumEntry
*/
implicit def materializeEnum[A <: EnumEntry]: Enum[A] = macro EnumMacros.materializeEnumImpl[A]

and can be used as such:

describe("materializeEnum") {
import DummyEnum._
def findEnum[A <: EnumEntry: Enum](v: A) = implicitly[Enum[A]]
it("should return the proper Enum object") {
val hello: DummyEnum = Hello
val companion = findEnum(hello)
companion shouldBe DummyEnum
companion.values should contain(Hello)
}

It seems like Enum itself is already the typeclass you're talking about?

When something is too useful and too obvious it probably exists already - A common Scala saying 😄

Thanks a lot for the pointer, haven't seen this implicit existed.
Closing this, and will open the Decoder idea as a separate thing