2016-01-19 2 views
7

J'ai certaines classes de cas qui ont une méthode tupled définie dans son objet compagnon. Comme il peut être vu à partir du code ci-dessous dans les objets compagnons, il s'agit juste de la duplication de code.Définir un trait à étendre par classe de cas dans scala

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 
} 

D'une autre question (can a scala self type enforce a case class type), il semble que nous ne peut pas appliquer l'auto-type d'un trait à une classe de cas.

Existe-t-il un moyen de définir un trait (disons Tupled) qui peut être appliqué comme suit?

// 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 { 
} 

Répondre

12

Parce qu'il n'y a pas de relation entre FunctionN types Scala, il est impossible de le faire sans des objets standard au niveau de arity quelque part, il n'y a aucun moyen de faire abstraction sur le compagnon apply méthodes sans énumérer tous les numéros possibles de membres.

Vous pouvez le faire à la main avec un tas de caractères CompanionN[A, B, C, ...], mais c'est assez ennuyeux. Shapeless fournit une meilleure solution, ce qui vous permet d'écrire quelque chose comme ce qui suit:

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)) 
} 

Et puis:

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] 

que vous pouvez utiliser comme ceci:

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) 

Vous pourriez ne veut même pas la partie CaseClassCompanion, puisqu'il est possible de construire une méthode générique qui convertit les tuples en classes de cas (en supposant que les types de membres s'alignent):

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] 

Et puis:

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) 

Cela fonctionne pour les classes de cas jusqu'à 22 membres (puisque la méthode apply sur l'objet compagnon ne peut pas être eta-étendu à une fonction s'il y a plus de 22 arguments).

+2

En outre, 'fromTuple' pourrait être un meilleur nom pour cette méthode - il est vrai que vous êtes en train de taper' apply', mais appeler le résultat 'tupled' fait c'est comme si la conversion était _to_ un tuple de _from_. –

+0

Merci beaucoup! – TheKojuEffect

+0

@TravisBrown Comment puis-je obtenir le même effet avec des classes de cas de 1 paramètre? – vicaba

2

Le problème est que la signature de apply diffère d'un cas à l'autre, et qu'il n'y a pas de trait commun pour ces fonctions. Book.tupled et Author.tupled ont fondamentalement le même code, mais ont des signatures très différentes. Par conséquent, la solution peut ne pas être aussi belle que nous le souhaiterions.


Je peux concevoir un moyen d'utiliser une macro d'annotation pour découper la plaquette. Comme il n'y a pas de bonne façon de le faire avec la librairie standard, je recourrai à la génération de code (qui a toujours la sécurité à la compilation). La mise en garde ici est que les macros d'annotation nécessitent l'utilisation du plugin de compilation macro paradise. Les macros doivent également être dans une unité de compilation séparée (comme un autre sous-projet sbt). Le code qui utilise l'annotation nécessiterait également l'utilisation du plugin macro paradise.

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) 
    } 

} 

Utilisation:

@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> 

Sinon, quelque chose comme cela peut être possible avec informes. Voir la meilleure réponse @ TravisBrown.

+0

Merci pour votre réponse. – TheKojuEffect