Define an attribute that should be extended by case class in scala

I have case classes that have a tupled method defined in its companion object. As you can see from the code below in the companion objects, this is just code duplication.

 case class Book(id: Int, isbn: String, name: String) object Book { def tupled = (Book.apply _).tupled // Duplication } case class Author(id: Int, name: String) object Author { def tupled = (Author.apply _).tupled // Duplication } 

From another question ( can the scala self type introduce the case class type ), it seems that we cannot force the attribute self-differential of the case class case.

Is there a way to define a feature (say tupled ) that can be applied as follows?

 // What would be value of ??? trait Tupled { self: ??? => def tupled = (self.apply _).tupled } // Such that I can replace tupled definition with Trait object Book extends Tupled { } 
+7
scala traits case-class self-type
source share
2 answers

Since there is no relation between FunctionN types in Scala, this cannot be done without a level-level template somewhere - there is simply no way to abstract from the methods of related apply objects without listing all possible number of members.

You can do this manually using the CompanionN[A, B, C, ...] feature set CompanionN[A, B, C, ...] , but it is rather annoying. Shapeless provides a much better solution for writing something like the following:

 import shapeless.{ Generic, HList }, shapeless.ops.product.ToHList class CaseClassCompanion[C] { def tupled[P <: Product, R <: HList](p: P)(implicit gen: Generic.Aux[C, R], toR: ToHList.Aux[P, R] ): C = gen.from(toR(p)) } 

And then:

 case class Book(id: Int, isbn: String, name: String) object Book extends CaseClassCompanion[Book] case class Author(id: Int, name: String) object Author extends CaseClassCompanion[Author] 

What you can use as follows:

 scala> Book.tupled((0, "some ISBN", "some name")) res0: Book = Book(0,some ISBN,some name) scala> Author.tupled((0, "some name")) res1: Author = Author(0,some name) 

Perhaps you don’t even need a part of CaseClassCompanion , since you can build a general method that converts tuples to case classes (provided that the type members line up):

 class PartiallyAppliedProductToCc[C] { def apply[P <: Product, R <: HList](p: P)(implicit gen: Generic.Aux[C, R], toR: ToHList.Aux[P, R] ): C = gen.from(toR(p)) } def productToCc[C]: PartiallyAppliedProductToCc[C] = new PartiallyAppliedProductToCc[C] 

And then:

 scala> productToCc[Book]((0, "some ISBN", "some name")) res2: Book = Book(0,some ISBN,some name) scala> productToCc[Author]((0, "some name")) res3: Author = Author(0,some name) 

This will work for case classes with up to 22 members (since the apply method on a companion object cannot be eta-extended to a function if there are more than 22 arguments).

+12
source share

The problem is that the apply signature is different from case to case and that there is no common feature for these functions. Book.tupled and Author.tupled basically have the same code, but have very different signatures. Therefore, the decision may not be as pleasant as we would like.


I can imagine a way to use an annotation macro to cut out a template. Since there is no suitable way to do this with the standard library, I will resort to generating code (which still remains safe at compile time). The caveat here is that annotation macros require the use of the macro paradise compiler. Macros should also be in a separate compilation unit (for example, in another sbt subproject). Code that uses annotation will also require the use of the macro paradise plugin.

 import scala.annotation.{ StaticAnnotation, compileTimeOnly } import scala.language.experimental.macros import scala.reflect.macros.whitebox.Context @compileTimeOnly("enable macro paradise to expand macro annotations") class Tupled extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro tupledMacroImpl.impl } object tupledMacroImpl { def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ val result = annottees map (_.tree) match { // A case class with companion object, we insert the `tupled` method into the object // and leave the case class alone. case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }") :: (objDef @ q"object $objName extends { ..$objEarlyDefs } with ..$objParents { $objSelf => ..$objDefs }") :: Nil if mods.hasFlag(Flag.CASE) => q""" $classDef object $objName extends { ..$objEarlyDefs } with ..$objParents { $objSelf => ..$objDefs def tupled = ($objName.apply _).tupled } """ case _ => c.abort(c.enclosingPosition, "Invalid annotation target: must be a companion object of a case class.") } c.Expr[Any](result) } } 

Using:

 @Tupled case class Author(id: Int, name: String) object Author // Exiting paste mode, now interpreting. defined class Author defined object Author scala> Author.tupled res0: ((Int, String)) => Author = <function1> 

Alternatively, something like this might be possible with the formless. See @TravisBrown for the best answer.

+2
source share

All Articles