0

I have a trait with objects that Im using to represent an enum like:

sealed trait status extends Product with Serializable

object status{
  case object pending extends status
  case object ready extends status
  type ready = ready.type
  type pending = pending.type
}

Then I have two case classes:

case class container[+S <: Status](status : S, commonValue: String)
case class notAContainer(status : Status, commonValue:String)

I want to be able to map my notAContainer class to a container class by using the value it has in Status. Is there anyway I can do that? I could also change the type of status within notAContainer.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
athomassi
  • 13
  • 2

1 Answers1

0

You can just do

val nc: notAContainer = notAContainer(status.pending, "abc")
val nc1: notAContainer = notAContainer(status.ready, "def")

container(nc.status, nc.commonValue)
container(nc1.status, nc1.commonValue)

You will have values of type container[status] (not of its subtypes container[status.pending], container[status.ready]).


Just in case, if it doesn't suit your use case please explain why (why you need values of types container[status.pending], container[status.ready], how you're going to use them etc.).


If this is really important (for example if the constructor of class container behaves differently for different S) then for example you can specify the type parameter and downcast

container[status.pending](nc.status.asInstanceOf[status.pending], nc.commonValue)
container[status.ready](nc1.status.asInstanceOf[status.ready], nc1.commonValue)

Or you can use pattern matching

nc.status match {
  case s: status.ready => container[status.ready](s, nc.commonValue)
  case s: status.pending => container[status.pending](s, nc.commonValue)
}

But the result will have type container[status].


You can even automate the pattern matching with a macro

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

def matchStatus(nc: notAContainer): container[status] = macro matchStatusImpl

def matchStatusImpl(c: blackbox.Context)(nc: c.Tree): c.Tree = {
  import c.universe._
  val s = TermName(c.freshName("s"))
  val cases = typeOf[status].typeSymbol.asClass.knownDirectSubclasses.map(symb => {
    val typ = symb.asType.toType
    val pattern = pq"$s: $typ"
    cq"$pattern => container.apply[$typ]($s, $nc.commonValue)"
  })
  q"""
    $nc.status match {
      case ..$cases
    }
  """
}

matchStatus(nc)
//scalac: App.this.nc.status match {
//  case (s$macro$1 @ (_: App1.status.pending.type)) => App1.container.apply[App1.status.pending.type](s$macro$1, App.this.nc.commonValue)
//  case (s$macro$1 @ (_: App1.status.ready.type)) => App1.container.apply[App1.status.ready.type](s$macro$1, App.this.nc.commonValue)
//}

Pattern matching (manual or with the macro) occurs at runtime. So at compile time we can't have a value of types container[status.pending], container[status.ready], only a value of type container[status].

If you really need a value of type container[status.pending] or container[status.ready] then you can use reflective compilation at runtime

import scala.reflect.runtime.{currentMirror => cm}
import scala.reflect.runtime.universe.Quasiquote
import scala.tools.reflect.ToolBox

object App {
  val tb = cm.mkToolBox()

  sealed trait status extends Product with Serializable

  object status{
    case object pending extends status
    case object ready extends status
    type ready = ready.type
    type pending = pending.type
  }

  case class container[+S <: status](status : S, commonValue: String)
  case class notAContainer(status : status, commonValue:String)

  val nc: notAContainer = notAContainer(status.pending, "abc")

  def main(args: Array[String]): Unit = {
//    tb.eval(tb.parse(
//      s"""import App._
//         |val c = container[status.${nc.status}](status.${nc.status}, "${nc.commonValue}")
//         |println(c)
//         |""".stripMargin))

//    val clazz = nc.status.getClass
//    val classSymbol = cm.classSymbol(clazz)
    val classSymbol = cm.reflect(nc.status).symbol
//    val moduleSymbol = cm.moduleSymbol(clazz)
    val moduleSymbol = classSymbol.owner.info.decl(classSymbol.name.toTermName) // (*)
    tb.eval(q"""
      import App._
      val c = container[${classSymbol.toType}]($moduleSymbol, ${nc.commonValue})
      println(c)
    """)
  }
}

(*) 1 2

Inside quasiquotes q"..." the variable c has type container[status.pending].

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • much appreciated @dmytro, ill take the time to review and implement to see if I can get this working for my needs. Thank you! – athomassi Oct 13 '21 at 22:35
  • @athomassi Did you have a chance to look whether this works for your use case? Is type `container[status]` enough or do you really need types `container[status.pending]`, `container[status.ready]`? In the latter case how are you going to use values of types `container[status.pending]`, `container[status.ready]`? – Dmytro Mitin Oct 20 '21 at 11:44