Come usare Scala per digitare, tipi astratti, ecc. Per implementare un Self type?

Non ho trovato la risposta a questo in nessun’altra domanda. Supponiamo di avere una superclass astratta Abstract0 con due sottoclassi, Concrete1 e Concrete1. Voglio essere in grado di definire in Abstract0 qualcosa del genere

def setOption(...): Self = {...} 

dove il Sé sarebbe il sottotipo concreto. Ciò consentirebbe di concatenare le chiamate a setOption in questo modo:

 val obj = new Concrete1.setOption(...).setOption(...) 

e ancora ottenere Concrete1 come il tipo di object inferito.

Quello che non voglio è definire questo:

 abstract class Abstract0[T <: Abstract0[T]] 

perché rende più difficile per i clienti gestire questo tipo. Ho provato varie possibilità tra cui un tipo astratto:

 abstract class Abstract0 { type Self <: Abstract0 } class Concrete1 extends Abstract0 { type Self = Concrete1 } 

ma allora è imansible implementare setOption, perché in Abstract0 non ha tipo Self. E usando this: Self => inoltre non funziona in Abstract0.

Quali soluzioni ci sono a questo problema?

Questo è ciò che questo this.type è per:

 scala> abstract class Abstract0 { | def setOption(j: Int): this.type | } defined class Abstract0 scala> class Concrete0 extends Abstract0 { | var i: Int = 0 | def setOption(j: Int) = {i = j; this} | } defined class Concrete0 scala> (new Concrete0).setOption(1).setOption(1) res72: Concrete0 = [email protected] 

Come puoi vedere, setOption restituisce il tipo effettivo usato, non Abstract0. Se Concrete0 avesse setOtherOption (new Concrete0).setOption(1).setOtherOption(...) funzionerebbe

AGGIORNAMENTO: rispondere alla domanda di risposta di JPP nel commento (come restituire nuove istanze: l’approccio generale descritto nella domanda è quello giusto (utilizzando i tipi astratti), tuttavia la creazione delle nuove istanze deve essere esplicita per ogni sottoclass.

Un approccio è:

 abstract class Abstract0 { type Self <: Abstract0 var i = 0 def copy(i: Int) : Self def setOption(j: Int): Self = copy(j) } class Concrete0(i: Int) extends Abstract0 { type Self = Concrete0 def copy(i: Int) = new Concrete0(i) } 

Un altro è quello di seguire il modello di builder utilizzato nella libreria di raccolta di Scala. Cioè, setOption riceve un parametro implicito del costruttore. Ciò presenta i vantaggi che la creazione della nuova istanza può essere eseguita con più metodi rispetto alla semplice 'copia' e che è ansible eseguire build complesse. Ad esempio un setSpecialOption può specificare che l'istanza di ritorno deve essere SpecialConcrete.

Ecco un'illustrazione della soluzione:

 trait Abstract0Builder[To] { def setOption(j: Int) def result: To } trait CanBuildAbstract0[From, To] { def apply(from: From): Abstract0Builder[To] } abstract class Abstract0 { type Self <: Abstract0 def self = this.asInstanceOf[Self] def setOption[To <: Abstract0](j: Int)(implicit cbf: CanBuildAbstract0[Self, To]): To = { val builder = cbf(self) builder.setOption(j) builder.result } } class Concrete0(i: Int) extends Abstract0 { type Self = Concrete0 } object Concrete0 { implicit def cbf = new CanBuildAbstract0[Concrete0, Concrete0] { def apply(from: Concrete0) = new Abstract0Builder[Concrete0] { var i = 0 def setOption(j: Int) = i = j def result = new Concrete0(i) } } } object Main { def main(args: Array[String]) { val c = new Concrete0(0).setOption(1) println("c is " + c.getClass) } } 

AGGIORNAMENTO 2: risposta al secondo commento di JPP. In caso di diversi livelli di nidificazione, utilizzare un parametro di tipo anziché membro di tipo e rendere Abstract0 in un tratto:

 trait Abstract0[+Self <: Abstract0[_]] { // ... } class Concrete0 extends Abstract0[Concrete0] { // .... } class RefinedConcrete0 extends Concrete0 with Abstract0[RefinedConcrete0] { // .... } 

Questo è il caso d’uso esatto di this.type . Sarebbe come:

 def setOption(...): this.type = { // Do stuff ... this }