Yann Moisan
Yann Moisan

Reputation: 8281

How to avoid calling asInstanceOf in Scala with family polymorphism

By design, we know for sure that we have an instance of HourlyDateFormat

How to avoid calling asInstanceOf in this case (i.e how to help the compiler to infer the type) ?

  sealed trait StorageLayout extends Product with Serializable
  case object Hourly         extends StorageLayout
  case object Daily          extends StorageLayout

  sealed trait DateFormat[S <: StorageLayout]

  sealed abstract class HourlyDateFormat extends DateFormat[Hourly.type] {
    def format(localDate: LocalDate): String         = ???
    def format(localDateTime: LocalDateTime): String = ???
  }

  sealed abstract class DailyDateFormat extends DateFormat[Daily.type] {
    def format(localDate: LocalDate): String = ???
  }

  class Log[S <: StorageLayout](storageLayout: S, dateFormat: DateFormat[S]) {
    def getPath(date: LocalDate): String =
      dateFormat match {
        case hdf: HourlyDateFormat => hdf.format(date)
        case ddf: DailyDateFormat  => ddf.format(date)
      }
    @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
    def getPath(date: LocalDateTime)(implicit ev: S =:= Hourly.type): String = {
      assert(ev == ev)
      dateFormat.asInstanceOf[HourlyDateFormat].format(date)
    }
  }

Upvotes: 3

Views: 263

Answers (3)

user
user

Reputation: 7604

A quick fix would be something like this:

class Log[S <: StorageLayout, D <: DateFormat[S]](storageLayout: S, dateFormat: D) {
  def getPath(date: LocalDateTime)(implicit ev: D <:< HourlyDateFormat): String =
    dateFormat.format(date)
}

However, I don't think you've designed this the right way. It'd probably be better to have a separate trait for each type of format. This makes it more scalable, since you don't need to add a case in your match expression for each different class, the right method gets selected automatically at runtime. You still have to use those evidence parameters, which I don't like, but it still feels cleaner to me.

Edit: I've updated the code so that everything extends FormatLocalDate and you only need an evidence parameter for getPath(LocalDateTime)

sealed trait FormatLocalDate[S <: StorageLayout] {
  def format(localDate: LocalDate): String
}
sealed trait FormatLocalDateTime[S <: StorageLayout] extends FormatLocalDate[S] {
  def format(localDate: LocalDateTime): String
}

sealed abstract class HourlyDateFormat extends FormatLocalDateTime[Hourly.type] {
  def format(localDate: LocalDate): String = ???
  def format(localDateTime: LocalDateTime): String = ???
}
sealed abstract class DailyDateFormat extends FormatLocalDate[Daily.type] {
  def format(localDate: LocalDate): String = ???
}

class Log[S <: StorageLayout, D <: FormatLocalDate[S]](storageLayout: S, dateFormat: D) {
  def getPath(date: LocalDate): String =
    dateFormat.format(date)

  def getPath(date: LocalDateTime)(implicit ev: D <:< FormatLocalDateTime[S]): String =
    dateFormat.format(date)
}

Scastie

Upvotes: 3

Dmytro Mitin
Dmytro Mitin

Reputation: 51648

Try to add one more implicit parameter

def getPath(date: LocalDateTime)(implicit ev: S =:= Hourly.type, ev1: DateFormat[S] =:= HourlyDateFormat): String = {
  //assert(ev == ev)
  dateFormat.format(date)
}

Assertion looks strange: assert(ev == ev).

Or just

def getPath(date: LocalDateTime)(implicit ev1: DateFormat[S] =:= HourlyDateFormat): String

Fixed version (I added one more type parameter, it's now similar to the first @user's version)

class Log[S <: StorageLayout, D <: DateFormat[S]](storageLayout: S, dateFormat: D) {
  def getPath(date: LocalDate): String =
    dateFormat match {
      case hdf: HourlyDateFormat => hdf.format(date)
      case ddf: DailyDateFormat  => ddf.format(date)
    }
  def getPath(date: LocalDateTime)(implicit
                                   ev: S =:= Hourly.type,
                                   ev1: D <:< HourlyDateFormat,
  ): String = {
    dateFormat.format(date)
  }
}

val log = new Log(Hourly, new HourlyDateFormat(){})
print(log.getPath(LocalDateTime.now()))

Upvotes: 5

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27535

Generally things like that are kind-of type-classy, so I would do it this way instead:

trait DailyFormatter[S] {
  def formatDate(localDate: LocalDate): String
}
trait HourlyFormatter[S] {
  def formatDateTime(localDateTime: LocalDateTime): String
}

implicit val dailyFormats: DailyFormatter[Daily]
implicit val hourFormats: DailyFormatter[Hourly] with HourlyFormatter[Hourly]

class Log[S <: StorageLayout](storageLayout: S, dateFormat: DateFormat[S]) {

  def getPath(date: LocalDate)(implicit formater: DailyFormatter[S]): String =
    formater.formatDate(date)

  def getPath(date: LocalDateTime)(implicit formater: HourlyFormatter[S]): String =
    formater.formatDateTime(date)
}

It has the advantage that you don't have to be aware of existence of types HourlyDateFormat and DailyDateFormat to make it work.

Upvotes: 5

Related Questions