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._
Maybe I'm misunderstanding, but Enum
has this:
enumeratum/enumeratum-core/src/main/scala/enumeratum/Enum.scala
Lines 190 to 192 in 737ffca
and can be used as such:
enumeratum/enumeratum-core/src/test/scala/enumeratum/EnumSpec.scala
Lines 564 to 573 in 87c4096
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