2016-01-19 45 views
7

Ho alcune classi di casi che hanno un metodo tupled definito nell'oggetto associato. Come si può vedere dal codice sottostante negli oggetti associati, è solo la duplicazione del codice.Definire un tratto da estendere per classe del caso in 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 
} 

Da un'altra domanda (can a scala self type enforce a case class type), sembra che non siamo in grado di far rispettare l'auto-tipo di un tratto di essere una classe case.

C'è un modo per definire un tratto (ad esempio Tupled) che può essere applicato come segue?

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

risposta

12

Perché non c'è alcuna relazione tra FunctionN tipi di Scala, non è possibile fare questo senza di livello arity boilerplate da qualche parte, non c'è proprio nessun modo per astrarre il compagno di oggetti apply metodi senza enumerare tutti i possibili numeri di membri.

Si potrebbe fare questo a mano con un gruppo di tratti CompanionN[A, B, C, ...], ma questo è piuttosto fastidioso. Shapeless fornisce una soluzione molto migliore, che permette di scrivere qualcosa di simile al seguente:

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

E poi:

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] 

che è possibile utilizzare in questo modo:

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) 

Potrebbero non voglio nemmeno la parte CaseClassCompanion, dal momento che è possibile costruire un metodo generico che converte tuple in classi di casi (supponendo che i tipi di membri siano allineati):

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] 

E poi:

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) 

Questo lavoro per classi case con fino a 22 membri (dato che il metodo apply sull'oggetto guidata non può essere eta-espansa a una funzione se esistono più di 22 argomenti).

+2

Come nota a margine, 'fromTuple' potrebbe essere un nome migliore per questo metodo-è vero che stai tupendo 'apply', ma chiamare il risultato' fa tupla' suona come se la conversione fosse _ a_ una tupla di _from_. –

+0

Grazie mille !!! – TheKojuEffect

+0

@TravisBrown Come posso ottenere lo stesso effetto con le classi case di 1 parametro? – vicaba

2

Il problema è che la firma di apply differisce da caso a caso e che non esiste alcun tratto comune per queste funzioni. Book.tupled e Author.tupled hanno fondamentalmente lo stesso codice, ma hanno firme molto diverse. Pertanto, la soluzione potrebbe non essere bella come vorremmo.


Posso concepire un modo utilizzando una macro di annotazione per ritagliare lo standard. Dal momento che non esiste un buon modo per farlo con la libreria standard, farò ricorso alla generazione del codice (che ha ancora sicurezza in fase di compilazione). L'avvertenza qui è che le macro di annotazione richiedono l'uso del plugin del compilatore macro paradise. Le macro devono anche essere in una unità di compilazione separata (come un altro sottoprogetto SBT). Il codice che utilizza l'annotazione richiederebbe anche l'uso del plugin per il paradiso della macro.

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> 

In alternativa, qualcosa di simile a questo può essere possibile con informe. Vedi la risposta migliore di @ TravisBrown.

+0

Grazie per la risposta. – TheKojuEffect