Yann Moisan
Yann Moisan

Reputation: 8281

How to model a relation in Scala between the key type and the corresponding value type

I want to model log files that can be hourly or daily

sealed trait Level
case object Hour extends Level
case object Day extends Level

sealed trait Log[L <: Level]

and I want a method that return all logs for given a level. So here is the signature

def byLevel[L <: Level](l: L) : Seq[Log[L]]

Given some concrete log instances (there are a lot more in real code) :

case object HourlyLog extends Log[Hour.type]
case object DailyLog extends Log[Day.type]

I've figured out the following implementation :

object Log {
  case class Pair[L <: Level](level : L, logs: Seq[Log[L]])
  val logs = Seq(
    Pair(Hour, Seq(HourlyLog)),
    Pair(Day, Seq(DailyLog))
  )

  def byLevel[L <: Level](l: L) : Seq[Log[L]] = logs.find(_.level == l).get.logs.asInstanceOf[Seq[Log[L]]]
}

My questions are :

Upvotes: 0

Views: 122

Answers (2)

Andrey Tyukin
Andrey Tyukin

Reputation: 44918

It seems that you are using simple object comparison on the companion objects anyway, so why complicating it all with types instead of treating Hour and Day as a good old enumeration? If you want to store Seq[DailyLog] and Seq[HourlyLog] in the same list, you will need an asInstanceOf like it or not.


Anyway, here are some things you could do to get rid of Pair and asInstanceOf:

  1. Forget the types altogether, treat Hour and Day as values of an enum (only values, no types)
  2. Use implicits to map type directly to the right list (only types, no values)
  3. Use strange mix of the two approaches (closest to your code, I'm not sure why you might want this, though)

All three approaches use neither Pair, nor asInstanceOf (one uses Map, though... That's what your Pair is doing anyway). Every one of the three walls of code is compilable on its own.


Only values, no types, essentially enum

sealed trait Level
case object Hour extends Level
case object Day extends Level

sealed trait Log
case object HourlyLog extends Log
case object DailyLog extends Log

object Log {
  val logs = Map[Level, Seq[Log]](
    Hour -> Seq(HourlyLog, HourlyLog, HourlyLog, HourlyLog),
    Day -> Seq(DailyLog)
  )

  def byLevel(l: Level): Seq[Log] = logs(l)
}

import Log._

println(byLevel(Hour))
println(byLevel(Day))

Output:

List(HourlyLog, HourlyLog, HourlyLog, HourlyLog)
List(DailyLog)

Only types, no object values, implicits

sealed trait Level
sealed trait Hour extends Level
sealed trait Day extends Level

abstract class Log[L <: Level]

case object HourlyLog extends Log[Hour]
case object DailyLog extends Log[Day]

object Log {
  case class Logs[L <: Level](val logs: Seq[Log[L]])
  implicit val hourlyLogs = Logs[Hour](Seq(
    HourlyLog, HourlyLog, HourlyLog, HourlyLog
  ))
  implicit val dailyLogs = Logs[Day](Seq(
    DailyLog
  ))

  def byLevel[L <: Level](implicit logs: Logs[L]): Seq[Log[L]] =
    logs.logs
}

import Log._
println(byLevel[Hour])
println(byLevel[Day])

Output:

List(HourlyLog, HourlyLog, HourlyLog, HourlyLog)
List(DailyLog)

Hybrid approach, everything mixed together

sealed trait Level
case object Hour extends Level
case object Day extends Level

sealed trait Log[L <: Level]

case object HourlyLog extends Log[Hour.type]
case object DailyLog extends Log[Day.type]

object Log {
  case class Logs[L <: Level](val logs: Seq[Log[L]])
  implicit val hourlyLogs = Logs[Hour.type](Seq(
    HourlyLog, HourlyLog, HourlyLog, HourlyLog
  ))
  implicit val dailyLogs = Logs[Day.type](Seq(
    DailyLog
  ))

  def byLevel[L <: Level](l: L)(implicit logs: Logs[L]): Seq[Log[L]] =
    logs.logs

}

import Log._
println(byLevel(Hour))
println(byLevel(Day))

Output:

List(HourlyLog, HourlyLog, HourlyLog, HourlyLog)
List(DailyLog)

Upvotes: 1

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27535

Depending on case there are different ways of getting rid of casting.

  1. if you want to treat Log[Hour] as special case of Log[Level] (so each time you need Log[Level] you can substitute it with Log[Hour]) use covariance: [+L <: Level]

    You will need it in order to HourlyLog to be treated as a subtype of Log (sealed trait Log[+L <: Level])

  2. when you have a case of something that is collection of potentially mixed subtypes, you can use collect:

    def byLevel[L <: Level](l: L): Seq[Log[L]] = logs.collect { case Pair(l2, logs: Seq[Log[L]]) if l2 == l => logs }.flatten
    

Well, you still need to annotate the type of logs, but it as a lot more clean and doesn't require you to use shapeless not any other magic.

As for getting rid of Pair wrapper - thing is on JVM you will have a type erasure. You will need to compare against something that is runtime checked. I can think of something like ClassTag, or TypeTag... but you would still need to store it somewhere (e.g. as an implicit constructor argument sealed trait Log[+L <: Level: ClassTag]) and then compare against it. So I am certain it is, but it would produce some boilerplate.

Alternatively you could filter/collect using HourlyLog, but then you would have to store it expicitly somewhere to map Hour to filtering by HourlyLog... and I am not sure it would be worth the effort. Probably if you do it on a regular basic it would be worth to roll out dedicated type class and providing implicits... but that is your call. Depending on you use case that might be overengineering and purity for the sake of purity... or something that keeps codebase maintainable. In my humble experience sometimes "dirty" solution does the job well enough.

Upvotes: 0

Related Questions