Home > Software engineering >  Invoke construcotr based on passed parameter
Invoke construcotr based on passed parameter

Time:02-05

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?

CodePudding user response:

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))
  • Related