Search code examples
scalaconstructor

Invoke constructor based on passed parameter


Constructing an object based on passed parameter. But the parameter is not String. I found the solution how to do it with String Scala instantiate objects from String classname but I believe that it can be done nicer. Let's say the following classes:

sealed trait Figure
object Figure {
    final case class Circle(radius: Double) extends Figure
    final case class Square(a: Double) extends Figure
}

And let's define a function (which doesn't make sense) which take a parameter based on I can invoke the proper constructor:

val construct123: Figure => Either[String, Figure] = (figure: Figure) => Right(figure.apply(1.23))

I want to invoke

construct123(Circle)
//or
construct123(Square)

Is it even possible?


Solution

  • The easiest would be to modify the signature of construct123 slightly

    def construct123(figure: Double => Figure): Either[String, Figure] =
      Right(figure(1.23))
    
    construct123(Circle.apply) // Right(Circle(1.23))
    construct123(Square.apply) // Right(Square(1.23))
    construct123(Circle(_)) // Right(Circle(1.23))
    construct123(Square(_)) // Right(Square(1.23))
    construct123(Circle) // Right(Circle(1.23))
    construct123(Square) // Right(Square(1.23))
    

    Or construct123 can be written as a higher-order function

    val construct123: (Double => Figure) => Either[String, Figure] =
      figure => Right(figure(1.23))
    

    Difference between method and function in Scala


    Circle and Square in construct123(Circle) and construct123(Square) are not the case classes Circle and Square but their companion objects

    Class companion object vs. case class itself

    So actually you want to transform an object into an instance of its companion class. You can do this for example with a macro

    // libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    def construct123[A](figure: A): Either[String, Figure] = macro construct123Impl[A]
    
    def construct123Impl[A: c.WeakTypeTag](c: blackbox.Context)(figure: c.Tree): c.Tree = {
      import c.universe._
      val companionClass = weakTypeOf[A].companion
      q"_root_.scala.Right.apply(new $companionClass(1.23))" // using constructor of the case class
    }
    

    or

    def construct123Impl[A: c.WeakTypeTag](c: blackbox.Context)(figure: c.Tree): c.Tree = {
      import c.universe._
      val A = symbolOf[A].asClass.module
      q"_root_.scala.Right.apply($A.apply(1.23))" // using apply method of the companion object
    }
    

    Testing (in a different subproject):

    construct123(Circle) // Right(Circle(1.23))
    construct123(Square) // Right(Square(1.23))
    
       // scalacOptions += "-Ymacro-debug-lite"
    //scalac: scala.Right.apply(new Macros.Figure.Circle(1.23))
    //scalac: scala.Right.apply(new Macros.Figure.Square(1.23))
    
    //scalac: scala.Right.apply(Circle.apply(1.23))
    //scalac: scala.Right.apply(Square.apply(1.23))
    

    You can hide the work with macros in type classes (defined with whitebox implicit macros). The type class ToCompanion below is similar to HasCompanion in Get companion object of class by given generic type Scala (answer). The type class Generic from Shapeless (used below to define construct123) is also macro-generated. Some intros to type classes (ordinary, not macro-generated): 1 2 3 4 5 6 7 8 9

    import scala.language.experimental.macros
    import scala.reflect.macros.whitebox
    
    // type class
    trait ToCompanion[A] {
      type Out
    }
    
    object ToCompanion {
      type Aux[A, Out0] = ToCompanion[A] {type Out = Out0}
      // materializer
      def apply[A](implicit tcc: ToCompanion[A]): ToCompanion.Aux[A, tcc.Out] = tcc
      // def apply[A](implicit tcc: ToCompanion[A]): tcc.type = tcc
    
      // instance of the type class
      implicit def mkToCompanion[A, B]: ToCompanion.Aux[A, B] = macro mkToCompanionImpl[A]
      // implicit def mkToCompanion[A]: ToCompanion[A] = macro mkToCompanionImpl[A] // then implicitly[ToCompanion.Aux[Circle, Circle.type]] doesn't compile in spite of white-boxity
    
      def mkToCompanionImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
        import c.universe._
        val A = weakTypeOf[A]
        val companion = A.companion
        val ToCompanion = weakTypeOf[ToCompanion[A]]
        q"new $ToCompanion { type Out = $companion }"
      }
    }
    
    implicitly[ToCompanion.Aux[Circle, Circle.type]] // compiles
    implicitly[ToCompanion.Aux[Circle.type, Circle]] // compiles
    
    val tc = ToCompanion[Circle.type]
    implicitly[tc.Out =:= Circle] // compiles
    val tc1 = ToCompanion[Circle]
    implicitly[tc1.Out =:= Circle.type] // compiles
    
    // libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
    import shapeless.{::, Generic, HNil}
    
    def construct123[A, B <: Figure](figure: A)(implicit
      toCompanion: ToCompanion.Aux[A, B],
      generic: Generic.Aux[B, Double :: HNil]
    ): Either[String, Figure] = Right(generic.from(1.23 :: HNil))
    
    construct123(Circle) // Right(Circle(1.23))
    construct123(Square) // Right(Square(1.23))
    

    Since all the classes are now known at compile time it's better to use compile-time reflection (the above macros). But in principle runtime reflection can be used too

    import scala.reflect.runtime.{currentMirror => rm}
    import scala.reflect.runtime.universe._
    
    def construct123[A: TypeTag](figure: A): Either[String, Figure] = {
      val classSymbol = symbolOf[A].companion.asClass
      //val classSymbol = typeOf[A].companion.typeSymbol.asClass
      val constructorSymbol = typeOf[A].companion.decl(termNames.CONSTRUCTOR).asMethod
      val res = rm.reflectClass(classSymbol).reflectConstructor(constructorSymbol)(1.23).asInstanceOf[Figure]
      Right(res)
    }
    

    or

    import scala.reflect.ClassTag
    import scala.reflect.runtime.{currentMirror => rm}
    import scala.reflect.runtime.universe._
    
    def construct123[A: TypeTag : ClassTag](figure: A): Either[String, Figure] = {
      val methodSymbol = typeOf[A].decl(TermName("apply")).asMethod
      val res = rm.reflect(figure).reflectMethod(methodSymbol).apply(1.23).asInstanceOf[Figure]
      Right(res)
    }
    

    Or you can use structural types aka duck typing (i.e. also runtime reflection under the hood)

    import scala.language.reflectiveCalls
    
    def construct123(figure: { def apply(x: Double): Figure }): Either[String, Figure] =
      Right(figure(1.23))