1

I have a sealed trait/abstract class hierarchy and need to convert its subtypes to strings and these strings back to the types. This code example describes what I want to achieve:

import shapeless._

object Test extends App {

  sealed abstract class C(val i: Int)
  case object O1 extends C(1)
  case object O2 extends C(2)
  // lots of other implementations

  trait TC[A] {
    def f: String
  }
  implicit object TC1 extends TC[O1.type] {
    def f = "O1"
  }
  implicit object TC2 extends TC[O2.type] {
    def f = "O2"
  }
  object fqn {
    val o1 = implicitly[TC[O1.type]].f
    val o2 = implicitly[TC[O2.type]].f

    def asString(c: C): String = c match {
      case O1 ⇒ o1
      case O2 ⇒ o2
    }

    def fromString(s: String): C = s match {
      case `o1` ⇒ O1
      case `o2` ⇒ O2
    }
  }

  object asString extends Poly1 {
    private implicit def impl[A <: C : TC] = at[A](a ⇒ implicitly[TC[A]].f)
    def apply(c: C): String = Generic[C].to(c).map(this).unify
  }
  object fromString {
    def apply(s: String): C = ???
  }

  // This works as expected
  println(O1 eq fqn.fromString(fqn.asString(O1)))
  println(O2 eq fqn.fromString(fqn.asString(O2)))

  // Does not yet work
  println(O1 eq fromString(asString(O1)))
  println(O2 eq fromString(asString(O2)))
}

Right now, everything works fine with the functionality in fqn, but it is cumbersome and difficult to maintain for more subtypes of C. With shapeless I managed to get away with the asString part but I'm stuck in finding a solution for the fromString part.

Can anyone think of a way in shapeless (or another library) to implement the fromString part?

Some more details about my example:

  • TC is provided by a library, I can't change it. The typeclasses are generated by macros in this library, therefore the TCN type classes don't directly exist.
  • I can't easily provide a macro that generates the implementation of fqn.asString and fqn.fromString, therefore I'm looking for a library that already supports this behavior.
kiritsuku
  • 52,967
  • 18
  • 114
  • 136

2 Answers2

0

Unfortunately I don't know how to solve your problem in compile time with macros\shapeless magic. But in run time the solution could be:

import reflect.runtime.{currentMirror => cm}
import scala.reflect.runtime.universe._

def fromString(s: String): C = {
  val caseObjects = weakTypeOf[C].baseClasses.
     flatMap(s => s.asClass.knownDirectSubclasses)
  val map = caseObjects.map(obj => (obj.name.toString, obj)).toMap
  cm.reflectModule(map(s).companionSymbol.asModule).instance match {
    case c: C => c
  }
}

Maybe you have to add isModule conditions to get only case objects.

Nikita
  • 4,435
  • 3
  • 24
  • 44
  • The problem with reflection is that it is slow and not threadsafe. I could come up with quite a verbose solution, see my own answer. – kiritsuku Apr 16 '15 at 17:59
0

I managed to come up with a solution:

import shapeless._

object GetAllSingletons {
  sealed trait AllSingletons[A, C <: Coproduct] {
    def values: List[A]
  }

  implicit def cnilSingletons[A]: AllSingletons[A, CNil] =
    new AllSingletons[A, CNil] {
      def values = Nil
    }

  implicit def coproductSingletons[A, H <: A, T <: Coproduct](implicit
    witness: Witness.Aux[H],
    tsc: AllSingletons[A, T]
  ): AllSingletons[A, H :+: T] =
    new AllSingletons[A, H :+: T] {
      def values = witness.value :: tsc.values
    }

  final class GetAllSingletones[A] {
    def apply[C <: Coproduct]()(implicit
      gen: Generic.Aux[A, C],
      singletons: AllSingletons[A, C]
    ): List[A] =
      singletons.values
  }

  def singletons[A] = new GetAllSingletones[A]
}

object Test extends App {

  sealed abstract class C(val i: Int)
  case object O1 extends C(1)
  case object O2 extends C(2)
  // lots of other implementations

  trait TC[A] {
    def f: String
  }
  implicit object TC1 extends TC[O1.type] {
    def f = "O1"
  }
  implicit object TC2 extends TC[O2.type] {
    def f = "O2"
  }

  object asString extends Poly1 {
    private implicit def impl[A <: C : TC] = at[A](a ⇒ implicitly[TC[A]].f)
    def apply(c: C): String = Generic[C].to(c).map(this).unify
  }
  object fromString {
    val singletons: Map[String, C] = GetAllSingletons.singletons[C]().map(s ⇒ asString(s) → s)(collection.breakOut)
    def apply(s: String): C = singletons(s)
  }

  println(O1 eq fromString(asString(O1)))
  println(O2 eq fromString(asString(O2)))
}

The definitions in GetAllSingletons are cumbersome, but I couldn't find a way to simplify it further. The idea comes from this question. Another question has an answer that provides a simpler solution, but it only works in a more limited way.

Community
  • 1
  • 1
kiritsuku
  • 52,967
  • 18
  • 114
  • 136