2016-06-11 63 views
5

Ho diverse classi tutte estendono lo stesso tratto e condividono tutte le funzionalità reciproche che dovrebbero cambiare il loro stato. Tuttavia mi chiedevo se c'è un modo migliore per implementare la stessa funzionalità.scala - modo idiomatico per cambiare lo stato della classe

esempio:

trait Breed 
case object Pincher extends Breed 
case object Haski extends Breed 

trait Foox{ 
    def age: Int 
    def addToAge(i: Int): Foox 
} 

case class Dog(breed: Breed, age: Int) extends Foox 
case class Person(name: String, age: Int) extends Foox 

voglio che addToAge restituirà lo stesso oggetto con l'int aggiuntivo, ovviamente posso implementare la stessa per ogni classe, che contraddice regola DRY:

case class Dog(breed: Breed, age: Int) extends Foox{ 
    def addToAge(i: Int) = copy(age = age + i) 
} 
case class Person(name: String, age: Int) extends Foox{ 
    def addToAge(i:Int) = copy(age = age + i) 
} 
  1. c'è un modo migliore per evitarlo?

  2. c'è un'opzione per evitare di ridefinire quell'età: Int in ogni caso classe e mantenere il suo stato (l'età è già definita nel tratto)?

+0

Ok, sono davvero curioso di questo. Questa domanda sembra in qualche modo simile a (ma non un duplicato di) [questo] (http://stackoverflow.com/questions/7227641/scala-how-can-i-make-my-immutable-classes-easier-to-subclass) e [this] (http://stackoverflow.com/questions/21560470/method-inheritance-in-immutable-classes). Qualcuno sa se le risposte fornite lì aiutano affatto con questa domanda? –

+0

È possibile che si stia violando il principio dell'immutabilità se si tenta di modificare lo stato dell'oggetto stesso. Pertanto 'copia' sembra un approccio. Lo stato dovrebbe invece essere gestito dal codice che utilizza questi oggetti, ad esempio aggiorna il proprio stato per utilizzare l'oggetto restituito da 'addToAge' – tuxdna

+0

Non credo ci sia una risposta facile qui. Il pensiero più vicino che mi viene in mente è usare una qualche forma di lente o usare la libreria informe.Devi abbandonare OO e lavorare con astrazioni più funzionali. Ad esempio, se tu avessi un 'map()' che semplicemente trasforma l'età e lascia tutto il resto come si otterrebbe in questo caso ciò di cui hai bisogno. – marios

risposta

5

Una possibile soluzione, che può coprire alcuni casi d'uso, è quello di utilizzare Lens es dalla shapeless libreria:

import shapeless._ 

abstract class Foox[T](
    implicit l: MkFieldLens.Aux[T, Witness.`'age`.T, Int] 
) { 
    self: T => 
    final private val ageLens = lens[T] >> 'age 

    def age: Int 
    def addToAge(i: Int): T = ageLens.modify(self)(_ + i) 
} 

case class Dog(breed: Breed, age: Int) extends Foox[Dog] 
case class Person(name: String, age: Int) extends Foox[Person] 

Si noti che per creare un Lens è necessario un implicito MkFieldLens, così è più facile per definire Foox come abstract class anziché trait. Altrimenti dovresti scrivere del codice in ogni bambino per fornire quello implicito.

Inoltre, non credo ci sia un modo per evitare di definire un age: Int in ogni bambino. Devi fornire l'età in qualche modo quando costruisci un'istanza, ad es. Dog(Pincher, 5), quindi devi avere quell'argomento costruttore per età lì.


Qualche spiegazione in più:

Prendendo in prestito da un Haskell Lens tutorial:

Un obiettivo è una prima classe riferimento ad una sezione di un certo tipo di dati. [...] Dato un obiettivo ci sono essenzialmente tre cose che potrebbe desiderare di fare

  1. Vedere le sottoparte
  2. Modificare l'intero cambiando la sottoparte
  3. Combinate questo obiettivo con un altro obiettivo di guardare ancora più profondo

il primo e il secondo danno luogo l'idea che le lenti sono getter e setter come si potrebbe avere su un oggetto.

La parte di modifica può essere utilizzata per implementare ciò che vogliamo fare con age.

La libreria Shapeless fornisce una sintassi graziosa e senza piastra per definire e utilizzare obiettivi per i campi della classe caso. The code example in the documentation è auto esplicativo, credo.

Il seguente codice per il campo age Da tale esempio:

final private val ageLens = lens[???] >> 'age 
def age: Int 
def addToAge(i: Int): ??? = ageLens.modify(self)(_ + i) 

Quale dovrebbe essere il tipo di ritorno di addToAge essere? Dovrebbe essere il tipo esatto della sottoclasse da cui viene chiamato questo metodo. Solitamente questo viene ottenuto con F-bounded polymorphism. Quindi abbiamo la seguente:

trait Foox[T] { self: T => // variation of F-bounded polymorphism 

    final private val ageLens = lens[T] >> 'age 

    def age: Int 
    def addToAge(i: Int): T = ageLens.modify(self)(_ + i) 
} 

T viene utilizzato lì come il tipo esatto del bambino, ed ogni classe che estende Foox[T] dovrebbe dotarsi di T (a causa della dichiarazione di auto-tipo self: T =>). Ad esempio:

case class Dog(/* ... */) extends Foox[Dog] 

Ora abbiamo bisogno di fare il lavoro di linea lens[T] >> 'age.

Analizziamo la firma del metodo >> per vedere che cosa ha bisogno per funzionare:

def >>(k: Witness)(implicit mkLens: MkFieldLens[A, k.T]): Lens[S, mkLens.Elem] 
  1. vediamo che l'argomento 'age viene implicitamente convertito in un shapeless.Witness. Witness rappresenta il tipo esatto di un valore specifico o, in altre parole, un valore a livello di carattere. Due diversi valori letterali, ad es. Symbol s 'age e 'foo, hanno diversi testimoni e quindi i loro tipi possono essere distinti.

    Shapeless fornisce una sintassi di invenzioni stravagante per ottenere un valore Witness. Per 'age simbolo:

    Witness.`'age` // Witness object 
    Witness.`'age`.T // Specific type of the 'age symbol 
    
  2. seguito dalla voce 1 e la firma >>, abbiamo bisogno di avere un implicito MkFieldLens a disposizione, per la classe T (il bambino case class) e il campo 'age:

    MkFieldLens[T, Witness.`'age`.T] 
    

    Il Il campo age dovrebbe anche avere il tipo Int. E 'possibile esprimere questa esigenza con l'Aux pattern comune in informe:

    MkFieldLens.Aux[T, Witness.`'age`.T, Int] 
    

E per fornire questa implicita in modo più naturale, come un argomento implicito, dobbiamo usare un abstract class invece di un trait.

+0

Era simile a quello che avevo in mente. Possiamo aggiungere anche le importazioni solo per rendere più facile a qualcuno la riproduzione in un repl? – marios

+1

@marios OK, aggiunto. 'shapeless._' è abbastanza per farlo funzionare. Inoltre, ha bisogno della definizione "Razza" dalla domanda originale. – Kolmar

+0

@Kolmar, grazie è esattamente quello che stavo cercando, puoi per favore approfondire un po 'di più su questo voodoo :)? – igx

Problemi correlati