Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I build an anonymous instance of a generic type?

Consider this simple class:

class MyClass(p: (String, String) *) {
    val params: Map[String, String] = p.toMap
}

And a class that extends it:

class SomeOtherClass(p: (String, String) *) extends MyClass(p: _*)

I would like to remove the constructor from MyClass to avoid carrying around the initialization parameters in everything that extends it (potentially many types). Instead, I'd prefer the companion objects of the types that extend MyClass to inherit some sort of builder method.

class MyClass {
    val param = Map.empty[String, String] // some member with a default value
}

trait MyClassBuilder[A <: MyClass] {
    def apply(p: (String, String) *): A = ???
}

Basically apply should do something like this:

new A {
    override val param = p.toMap
}

Obviously, the above code cannot work. The idea is that a sub-type would be used like this:

class SomeOtherClass extends MyClass
object SomeOtherClass extends MyClassBuilder[SomeOtherClass] {
    // object specific things..
}

Then SomeOtherClass would rely on the inherited apply method for creating instances of itself. It seems something like this may be possible with reflection or macros, but I really have no idea. To be clear, I want client code like SomeOtherClass to require no constructor at all, which is why I want to explore creating anonymous classes on a generic type. It might not be SomeOtherClass that gets generated, it could be anything that extends MyClass.

like image 693
Michael Zajac Avatar asked Nov 10 '22 18:11

Michael Zajac


1 Answers

This may help you:

import scala.reflect._
abstract class MyClassBuilder[A <: MyClass: ClassTag] { 
  def apply() = classTag[A].runtimeClass.newInstance.asInstanceOf[A]
}

You may find that I didn't pass (String, String) * here as they're redundant for instantiation. Anyway, you have an access to the whole A's instance here, so at least you can change the param after instantiation. It's not exactly what you want - as you seems to looking for a way to additionaly extend SomeOtherClass in runtime. However, runtime creation of new type in scala is impossible, but you may think of it as a prototype - just take SomeOtherClass instance and mutate once inside apply():

import scala.reflect._
object Ctx {
  class MyClass {
    private[Ctx] var _param = Map.empty[String, String]
    def param = _param 
  }

  abstract class MyClassBuilder[A <: MyClass: ClassTag] { 
    def apply(p: (String, String) *) = {
       val i = classTag[A].runtimeClass.newInstance.asInstanceOf[A]
       i._param = Map(p: _*)
       i
    }
  }
}

scala> class MySpecialClass extends Ctx.MyClass
defined class MySpecialClass

scala> object MySpecialBuilder extends Ctx.MyClassBuilder[MySpecialClass]
defined module MySpecialBuilder

scala> MySpecialBuilder("A" -> "b")
res12: MySpecialClass = MySpecialClass@2871ed4a

scala> res12.param
res13: scala.collection.immutable.Map[String,String] = Map(A -> b)

Otherwise you have to deal with compile-time reflection.

An interesting alternative is extensible records (Shapeless2) - they practically allow you to build new type piece by piece, but you will have to deal with HList instead of class there:

import shapeless._ ; import syntax.singleton._ ; import record._
import shapeless.ops.record.Updater
import scala.reflect.runtime.universe._

val param = Witness("param")
val someFunW = Witness("someFun")

//defining classess (instead of "class MyClass")

type Param = Map[String, String] with KeyTag[param.T, Map[String, String]]
type SomeFun = (String => String) with KeyTag[someFunW.T, (String => String)]
type MyClass = Param :: HNil
type MySpecialClass = SomeFun :: MyClass

def someFun(s: String) = s + "A"

//defining default values (instead of "val param = ...")

def newMyClass[T <: HList : TypeTag]: T = ( 
   if (typeTag[T] == typeTag[MyClass]) 
      (param ->> Map.empty[String, String]) :: HNil 
   else if (typeTag[T] == typeTag[MySpecialClass]) 
      (someFunW ->> someFun _) :: newMyClass[MyClass]
   else HNil
).asInstanceOf[T]

//Defining builder
def buildWithParam[T <: HList: TypeTag](p: Map[String, String])(implicit ev: Updater[T, Param]) 
  = newMyClass[T] + ("param" ->> p)

scala> buildWithParam[MySpecialClass](Map("a" -> "v"))
res6: ... = <function1> :: Map(a -> v) :: HNil

scala> res6("someFun").apply("a")
res7: String = aA

scala> buildWithParam[MyClass](Map("a" -> "v"))
res8: ... = Map(a -> v) :: HNil
like image 194
dk14 Avatar answered Nov 14 '22 22:11

dk14