Define a trait to be extended by case class in scala
Asked Answered
R

2

7

I have some case classes which have a method tupled defined in its companion object. As it can be seen from the code below in companion objects, it 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 a scala self type enforce a case class type), it seems like we can not enforce the self-type of a trait to be a case class.

Is there a way to define a trait (say Tupled) that can be applied as following?

// 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 {
}
Retort answered 19/1, 2016 at 15:29 Comment(0)
F
12

Because there's no relationship between FunctionN types in Scala, it's not possible to do this without arity-level boilerplate somewhere—there's just no way to abstract over the companion objects' apply methods without enumerating all the possible numbers of members.

You could do this by hand with a bunch of CompanionN[A, B, C, ...] traits, but that's pretty annoying. Shapeless provides a much better solution, which allows you to write 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]

Which you can use like this:

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)

You might not even want the CaseClassCompanion part, since it's possible to construct a generic method that converts tuples to case classes (assuming the member types 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 the companion object can't be eta-expanded to a function if there are more than 22 arguments).

Frasquito answered 19/1, 2016 at 16:30 Comment(2)
As a side note, fromTuple might be a better name for this method—it's true that you're tupling the apply, but calling the result tupled makes is sound like the conversion is to a tuple instead of from.Frasquito
@TravisBrown How can I achieve the same effect with case classes of 1 parameter?Immaculate
S
2

The problem is that the signature of apply differs from case to case, and that there is no common trait for these functions. Book.tupled and Author.tupled basically have the same code, but have very different signatures. Therefore, the solution may not be as nice as we'd like.


I can conceive of a way using an annotation macro to cut out the boilerplate. Since there isn't a nice way to do it with the standard library, I'll resort to code generation (which still has compile-time safety). The caveat here is that annotation macros require the use of the macro paradise compiler plugin. Macros must also be in a separate compilation unit (like another sbt sub-project). Code that uses the annotation would 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)
  }

}

Usage:

@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 may be possible with shapeless. See @TravisBrown's better answer.

Schmitt answered 19/1, 2016 at 16:32 Comment(1)
Thanks for your answer.Retort

© 2022 - 2024 — McMap. All rights reserved.