Adding a Typeclass to Java enum - without simulacr

2019-09-12 11:50发布

问题:

I am trying to convert a couple of Java enumerations values into another Java enumeration (the usage of Java enum over Scala Enumeration is due to legacy reasons - the enums are actually generated using JAXB).

Instead of a plain old pattern matching and having a map mapping one enumeration type to another, I figured writing a typeclass looks cleaner (also kinda cool). When I use simulacrum to do this, it compiles and runs just great. However, when I try to hand-code the typeclass myself, it throws a compilation error

[error] /Users/arun/IdeaProjects/AdvancedScala/src/main/scala/MultipleToSingleEnum.scala:32: value toEmail is not a member of TradeEnum
[error]     println (TradeEnum.CLEARED.toEmail)

The code for the Java enumerations are :

Source enumerations

public enum TradeEnum {
    CONFIRMED, CLEARED
}

public enum SeriesEnum {
    CREATED,DELETED
}

Target enumeration

public enum EmailEnum {
    T_CONFIRMED, T_CLEARED, S_CREATED, S_DELETED
}

Typeclass using Simulacrum (Works just fine !)

import simulacrum._


@typeclass trait EmailEnumConvertibleSim[A]{
  def toEmailEnum(value:A):Option[EmailEnum]
}

object EmailEnumConvertibleSim{
  implicit val tradeToEmailEnum = new EmailEnumConvertibleSim[TradeEnum]{
    private val map=Map(
      TradeEnum.CLEARED -> EmailEnum.T_CLEARED,
      TradeEnum.CONFIRMED -> EmailEnum.T_CONFIRMED
    )
    override def toEmailEnum(value: TradeEnum): Option[EmailEnum] = map.get(value)
  }

  implicit val seriesToEmailEnum = new EmailEnumConvertibleSim[SeriesEnum]{
    private val map=Map(
      SeriesEnum.CREATED -> EmailEnum.S_CREATED,
      SeriesEnum.DELETED -> EmailEnum.S_DELETED
    )
    override def toEmailEnum(value: SeriesEnum): Option[EmailEnum] = map.get(value)
  }
}

import EmailEnumConvertibleSim.ops._

object MultipleToSingleEnumSim {
  def main(args: Array[String]): Unit = {
    println (TradeEnum.CLEARED.toEmailEnum)
  }
}

Hand-coded type class (Ops)

trait EmailEnumConvertible[A]{
  def toEmailEnum(value:A):Option[EmailEnum]
}

object EmailEnumConvertible{
  implicit val tradeToEmailEnum = new EmailEnumConvertible[TradeEnum]{
    private val map=Map(
      TradeEnum.CLEARED -> EmailEnum.T_CLEARED,
      TradeEnum.CONFIRMED -> EmailEnum.T_CONFIRMED
    )
    override def toEmailEnum(value: TradeEnum): Option[EmailEnum] = map.get(value)
  }

}

object EmailEnumOps{
  implicit class EmailEnumOps[A] (value:A){
    def toEmail()(implicit emailConvertable:EmailEnumConvertible[A]):Option[EmailEnum]={
      emailConvertable.toEmailEnum(value)
    }
  }
}

import EmailEnumOps._

object MultipleToSingleEnum {
  def main(args: Array[String]): Unit = {
    println (TradeEnum.CLEARED.toEmail) //ERROR IS REPORTED HERE !!
  }
}

Any light on the error message is highly appreciated.

回答1:

It's because your implicit class and the object it's defined in are both called EmailEnumOps.

When you change the name of the object it works:

trait EmailEnumConvertible[A]{
  def toEmailEnum(value: A): Option[EmailEnum]
}

object EmailEnumConvertible{
  implicit val tradeToEmailEnum: EmailEnumConvertible[TradeEnum] = new EmailEnumConvertible[TradeEnum]{
    private val map = Map(
      TradeEnum.CLEARED -> EmailEnum.T_CLEARED,
      TradeEnum.CONFIRMED -> EmailEnum.T_CONFIRMED
    )
    override def toEmailEnum(value: TradeEnum): Option[EmailEnum] = map.get(value)
  }

}

object AnyOtherName{
  implicit class EmailEnumOps[A] (value: A){
    def toEmail()(implicit emailConvertable:EmailEnumConvertible[A]): Option[EmailEnum]={
      emailConvertable.toEmailEnum(value)
    }
  }
}

import AnyOtherName._

object MultipleToSingleEnum {
  def main(args: Array[String]): Unit = {
    println (TradeEnum.CLEARED.toEmail) // No error :-)
  }
}

It looks like that when an object is defined in the current scope and then you import a member with the same name, the first object is still shadowing the imported member.

scala> :paste
// Entering paste mode (ctrl-D to finish)

object A { def B(a: Int) = "foo" }
object B 
import A._
B(4)

// Exiting paste mode, now interpreting.

<pastie>:41: error: B.type does not take parameters
       B(4)
        ^

An implicit class EmailEnumOps is compiled into a class EmailEnumOps and an implicit def EmailEnumOps. So when you imported EmailEnumOps._ that implicit def was shadowed by the object, and consequently TradeEnum.CLEARED could not be implicitly converted.

This behaviour is as specified in the language specification:

Bindings of different kinds have a precedence defined on them:

  1. Definitions and declarations that are local, inherited, or made available by a package clause and also defined in the same compilation unit as the reference, have highest precedence.

  2. Explicit imports have next highest precedence.

  3. Wildcard imports have next highest precedence.

  4. Definitions made available by a package clause, but not also defined in the same compilation unit as the reference, have lowest precedence.