scalaclassboilerplatecompanion-object

Case class and companion object sharing namespace and implementation?


So, It is not a global problem. But... its like a DRY problem. Lets introduce it. If I have a trait with some abstract member

trait WithSomeValue {
val someValue: Int
}

and some structure with case classes and their companions both inherited from this trait, if I have to make some static thing like a value and save it in Static(companion), like this:

object Foo extends WithSomeValue {
override val someValue: Int = 1
}

I need to assign it while implement trait in case class too

case class Foo(val x: Int) extends WithSomeValue {
override val someValue: Int = Foo.somevalue// I don`t like to write this! I feel myself DRY and tired ;)
} 

What can I do? Can I do something elegant not to write this assignment again and again? To write it in one place only?

I need to have available static members in static context and I have to get an hierarchy - trait->case class both? but I don`t want to write DRY code. I hope some trick exists, to avoid code duplication.


Solution

  • For example you can override the member in a common parent of the case class and its companion object

    trait WithSomeValue {
      val someValue: Int
    }
    
    trait WithSomeValueImpl extends WithSomeValue {
      override val someValue: Int = 1
    }
    
    object Foo extends WithSomeValueImpl
    case class Foo(x: Int) extends WithSomeValueImpl
    
    Foo.someValue     // 1
    Foo(42).someValue // 1
    

    I don't need one static value for all hierarchy but one value per object companion

    What about

    trait WithSomeValue {
      val someValue: Int
    }
    
    trait FooLike extends WithSomeValue {
      override val someValue: Int = 1
    }
    
    object Foo extends FooLike
    case class Foo(x: Int) extends FooLike
    
    trait BarLike extends WithSomeValue {
      override val someValue: Int = 2
    }
    
    object Bar extends BarLike
    case class Bar(x: Int) extends BarLike
    

    ?


    Actually, this is not a code duplication because a case class and its companion object are absolutely different classes

    Class companion object vs. case class itself

    someValue can be implemented in them absolutely differently.

    Anyway you can use the following macro annotation (see sbt settings below). It generates the member in the companion object copying the member from a class. If the companion object doesn't exist the annotation creates it.

    import scala.annotation.{StaticAnnotation, compileTimeOnly}
    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    @compileTimeOnly("enable macro annotations")
    class companionSomeValue extends StaticAnnotation {
      def macroTransform(annottees: Any*): Any = macro CompanionSomeValueMacros.macroTransformImpl
    }
    
    object CompanionSomeValueMacros {
      def macroTransformImpl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
        import c.universe._
    
        def modify(cls: ClassDef, obj: ModuleDef): Tree = (cls, obj) match {
          case (
            q"$_ class $_[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$stats }",
            q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }"
          ) =>
            val someValue = stats.collectFirst {
              case t@q"$_ val someValue: $_ = $_" => t
            }.get
    
            q"""
              $cls
    
              $mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
                ..$body
                $someValue
              }
            """
        }
    
        annottees match {
          case (cls: ClassDef) :: (obj: ModuleDef) :: Nil => modify(cls, obj)
          case (cls: ClassDef) :: Nil => modify(cls, q"object ${cls.name.toTermName} extends WithSomeValue")
        }
      }
    }
    
    // in a different subproject
    
    trait WithSomeValue {
      val someValue: Int
    }
    
    @companionSomeValue
    case class Foo(x: Int) extends WithSomeValue {
      override val someValue: Int = 1
    }
    object Foo extends WithSomeValue
    
    @companionSomeValue
    case class Bar(x: Int) extends WithSomeValue {
      override val someValue: Int = 2
    }
    
    Foo.someValue     // 1
    Foo(42).someValue // 1
    Bar.someValue     // 2
    Bar(42).someValue // 2
    
    //scalac: {
    //  case class Foo extends WithSomeValue with scala.Product with scala.Serializable {
    //    <caseaccessor> <paramaccessor> val x: Int = _;
    //    def <init>(x: Int) = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue: Int = 1
    //  };
    //  object Foo extends WithSomeValue {
    //    def <init>() = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue: Int = 1
    //  };
    //  ()
    //}
    
    //scalac: {
    //  case class Bar extends WithSomeValue with scala.Product with scala.Serializable {
    //    <caseaccessor> <paramaccessor> val x: Int = _;
    //    def <init>(x: Int) = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue: Int = 2
    //  };
    //  object Bar extends WithSomeValue {
    //    def <init>() = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue: Int = 2
    //  };
    //  ()
    //}
    

    Slightly different implementation. The annotation generates the member in a class delegating to the member of the companion object.

    @compileTimeOnly("enable macro annotations")
    class someValueFromCompanion extends StaticAnnotation {
      def macroTransform(annottees: Any*): Any = macro SomeValueFromCompanionMacros.macroTransformImpl
    }
    
    object SomeValueFromCompanionMacros {
      def macroTransformImpl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
        import c.universe._
    
        def modifyClass(cls: ClassDef): ClassDef = cls match {
          case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" =>
            q"""
              $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
                ..$stats
                override val someValue = ${tpname.toTermName}.someValue
              }
            """
        }
    
        def modify(cls: ClassDef, obj: ModuleDef): Tree = q"..${Seq(modifyClass(cls), obj)}"
    
        annottees match {
          case (cls: ClassDef) :: (obj: ModuleDef) :: Nil => modify(cls, obj)
        }
      }
    }
    
    @someValueFromCompanion
    case class Foo(x: Int) extends WithSomeValue
    object Foo extends WithSomeValue {
      override val someValue: Int = 1
    }
    
    @someValueFromCompanion
    case class Bar(x: Int) extends WithSomeValue
    object Bar extends WithSomeValue {
      override val someValue: Int = 2
    }
    
    //scalac: {
    //  case class Foo extends WithSomeValue with scala.Product with scala.Serializable {
    //    <caseaccessor> <paramaccessor> val x: Int = _;
    //    def <init>(x: Int) = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue = Foo.someValue
    //  };
    //  object Foo extends WithSomeValue {
    //    def <init>() = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue: Int = 1
    //  };
    //  ()
    //}
    
    //scalac: {
    //  case class Bar extends WithSomeValue with scala.Product with scala.Serializable {
    //    <caseaccessor> <paramaccessor> val x: Int = _;
    //    def <init>(x: Int) = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue = Bar.someValue
    //  };
    //  object Bar extends WithSomeValue {
    //    def <init>() = {
    //      super.<init>();
    //      ()
    //    };
    //    override val someValue: Int = 2
    //  };
    //  ()
    //}
    

    Auto-Generate Companion Object for Case Class in Scala (sbt settings for macro annotations)

    automatically generate case object for case class

    Generate companion object for case class with methods (field = method)