Comment utiliser cette typologie, ces types abstraits, etc. de Scala pour implémenter un type Self?

Je n’ai pas pu trouver la réponse à cela dans aucune autre question. Supposons que j’ai une super-classe abstraite Abstract0 avec deux sous-classes, Concrete1 et Concrete1. Je veux pouvoir définir dans Abstract0 quelque chose comme

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

où Self serait le sous-type concret. Cela permettrait de chaîner les appels à setOption comme ceci:

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

et toujours obtenir Concrete1 comme type d’object inféré.

Ce que je ne veux pas, c’est définir ceci:

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

car il est plus difficile pour les clients de gérer ce type. J’ai essayé différentes possibilités, y compris un type abstrait:

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

mais alors il est impossible d’implémenter setOption, car this dans Abstract0 n’a pas le type Self. Et en utilisant this: Self => ne fonctionne pas non plus dans Abstract0.

Quelles sont les solutions à ce problème?

C’est à quoi this.type :

 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 = Concrete0@a50ea1 

Comme vous pouvez le voir, setOption renvoie le type réel utilisé, pas abstract0. Si Concrete0 avait setOtherOption alors (new Concrete0).setOption(1).setOtherOption(...) fonctionnerait

UPDATE: pour répondre à la question de suivi de JPP dans le commentaire (comment retourner de nouvelles instances: L’approche générale décrite dans la question est la bonne (en utilisant des types abstraits). Cependant, la création des nouvelles instances doit être explicite pour chaque sous-classe.)

Une approche est la suivante:

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

Une autre consiste à suivre le modèle de générateur utilisé dans la bibliothèque de collections de Scala. C'est-à-dire que setOption reçoit un paramètre de générateur implicite. Cela présente l'avantage que la création de la nouvelle instance peut se faire avec plus de méthodes que la simple «copie» et que des constructions complexes peuvent être effectuées. Par exemple, setSpecialOption peut spécifier que l'instance de retour doit être SpecialConcrete.

Voici une illustration de la solution:

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

MISE À JOUR 2: Répondre au deuxième commentaire de JPP. Dans le cas de plusieurs niveaux d'imbrication, utilisez un paramètre de type à la place de type member et transformez Abstract0 en trait:

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

C’est le cas d’utilisation exact de this.type . Ce serait comme:

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