15

Supposons que l'on souhaite créer une nouvelle classe générique, Novel[A]. Cette classe contiendra beaucoup de méthodes utiles - c'est peut-être un type de collection - et donc vous voulez la sous-classer. Mais vous voulez que les méthodes retournent le type de la sous-classe, pas le type d'origine. Dans Scala 2.8, quelle est la quantité minimale de travail à faire pour que les méthodes de cette classe retournent la sous-classe pertinente, pas l'original? Par exemple,Structure minimale dans Scala pour les collections avec type de retour héritant

class Novel[A] /* What goes here? */ { 
    /* Must you have stuff here? */ 
    def reverse/* What goes here instead of :Novel[A]? */ = //... 
    def revrev/*?*/ = reverse.reverse 
} 
class ShortStory[A] extends Novel[A] /* What goes here? */ { 
    override def reverse: /*?*/ = //... 
} 
val ss = new ShortStory[String] 
val ss2 = ss.revrev // Type had better be ShortStory[String], not Novel[String] 

Est-ce que ce changement de montant minimal si vous voulez Novel à Covariant? (Les 2.8 collections font cela entre autres choses, mais elles jouent aussi avec les types de retour de façon plus fantaisiste (et utile) - la question est de savoir comment on peut se passer d'un framework si on veut seulement ces sous-types. -return-sous-types fonctionnalité.)

Modifier: Supposons dans le code ci-dessus que reverse fait une copie. Si on fait une modification sur place et qu'on se retourne ensuite, on peut utiliser this.type, mais cela ne fonctionne pas car la copie n'est pas this.

Arjan liée à une autre question qui suggère la solution suivante:

def reverse: this.type = { 
    /*creation of new object*/.asInstanceOf[this.type] 
} 

qui se trouve essentiellement au système de type afin d'obtenir ce que nous voulons. Mais ce n'est pas vraiment une solution, parce que maintenant que nous avons menti au système de type, le compilateur ne peut pas nous aider à nous assurer que nous avons effectivement un quand nous pensons que nous le faisons. (Par exemple, nous n'aurions pas à surcharger reverse dans l'exemple ci-dessus pour rendre le compilateur heureux, mais nos types ne seraient pas ce que nous voulions.)

+3

Vous ne vouliez probablement pas dire les types Singleton de Scala comme exemple du cadre minimal que vous aviez en tête, je présume? Dans tous les cas, des exemples en ligne pour montrer comment il peut être utilisé peuvent être trouvés à http://scalada.blogspot.com/2008/02/thistype-for-chaining-method-calls.html. Il y a aussi un exemple sur SO qui montre comment des classes immuables peuvent être produites avec un peu de hack http://stackoverflow.com/questions/775312/why-cannot-this-type-be-used-for-new-instances –

+0

@Arjan - Je ne voulais pas dire cela, car c'est trop limitatif. (Les méthodes de copie sont interdites, par exemple.) –

Répondre

3

Je n'ai pas réfléchi à cela complètement, mais la vérification des type:

object invariant { 
    trait Novel[A] { 
    type Repr[X] <: Novel[X] 

    def reverse: Repr[A] 

    def revrev: Repr[A]#Repr[A] 
     = reverse.reverse 
    } 
    class ShortStory[A] extends Novel[A] { 
    type Repr[X] = ShortStory[X] 

    def reverse = this 
    } 

    val ss = new ShortStory[String] 
    val ss2: ShortStory[String] = ss.revrev 
} 

object covariant { 
    trait Novel[+A] { 
    type Repr[X] <: Novel[_ <: X] 

    def reverse: Repr[_ <: A] 

    def revrev: Repr[_ <: A]#Repr[_ <: A] = reverse.reverse 
    } 

    class ShortStory[+A] extends Novel[A] { 
    type Repr[X] = ShortStory[X] 

    def reverse = this 
    } 

    val ss = new ShortStory[String] 
    val ss2: ShortStory[String] = ss.revrev 
} 

EDIT

La version co-variante peut être beaucoup plus agréable:

object covariant2 { 
    trait Novel[+A] { 
    type Repr[+X] <: Novel[X] 

    def reverse: Repr[A] 

    def revrev: Repr[A]#Repr[A] = reverse.reverse 
    } 

    class ShortStory[+A] extends Novel[A] { 
    type Repr[+X] = ShortStory[X] 

    def reverse = this 
    } 

    val ss = new ShortStory[String] 
    val ss2: ShortStory[String] = ss.revrev 
} 
+0

Cela semble prometteur, mais que faire si je veux sous-classer 'ShortStory'? Le problème est que je pourrais avoir un code d'initialisation utile dans une classe et vouloir le sous-classer sans perdre ce travail - donc je préférerais ne pas faire tout descendre des traits. Cependant, si la réponse est que l'on ne peut pas obtenir le système de type pour accepter mes demandes, cela ressemble à un prix de consolation raisonnable. –

5

Edit: Je viens de réaliser que Rex avait un roman de classe concret dans son exemple, pas un trait comme je l'ai utilisé ci-dessous. La mise en œuvre du trait est un peu trop simple pour être une solution à la question de Rex. Cela peut être fait aussi bien en utilisant une classe concrète (voir ci-dessous), mais la seule façon de faire ce travail est de faire un casting, ce qui fait que ce n'est pas vraiment une compilation de type time safe. Cela ne peut donc pas être considéré comme une solution.

Peut-être pas la plus jolie, mais un exemple simple en utilisant des types de membres abstraits pourrait être mis en œuvre comme suit:


trait Novel[A] { 
    type T <: Novel[A] 
    def reverse : T 
    def revrev : T#T = reverse.reverse 
} 

class ShortStory[A](var story: String) extends Novel[A] { 
type T = ShortStory[A] 
def reverse : T = new ShortStory[A](story reverse) 
def myMethod: Unit = println("a short story method") 
} 

scala> val ss1 = new ShortStory[String]("the story so far") 
ss1: ShortStory[String] = [email protected] 

scala> val ssRev = ss1 reverse 
ssRev: ss1.T = [email protected] 

scala> ssRev story 
res0: String = raf os yrots eht 

scala> val ssRevRev = ss1 revrev 
ssRevRev: ss1.T#T = [email protected] 

scala> ssRevRev story 
res1: String = the story so far 

scala> ssRevRev myMethod 
a short story method 

Il est certainement minime, mais je doute que cela suffisant pour être utilisé comme une sorte de cadre. Et bien sûr, les types retournés ne sont pas aussi clairs que dans le cadre des collections Scala, alors peut-être que c'est un peu trop simple. Pour le cas donné, il semble faire le travail, cependant. Comme mentionné ci-dessus, cela ne fait pas le travail pour le cas donné, donc une autre solution est requise ici.

Encore un autre Edit: Quelque chose de semblable peut être fait en utilisant une classe concrète aussi bien, mais cela suffit aussi de ne pas être de type sûr:


class Novel[A](var story: String) { 
    type T <: Novel[A] 
    def reverse: T = new Novel[A](story reverse).asInstanceOf[T] 
    def revrev : T#T = reverse.reverse 
} 
class ShortStory[A](var s: String) extends Novel[A](s) { 
type T = ShortStory[A] 
override def reverse : T = new ShortStory(story reverse) 
def myMethod: Unit = println("a short story method") 
} 

Et le code fonctionnera comme dans l'exemple trait. Mais il souffre du même problème que Rex mentionné dans son édition aussi bien. Le remplacement de ShortStory n'est pas nécessaire pour faire cette compilation. Cependant, il échouera à l'exécution si vous ne le faites pas et appelez la méthode inverse sur une instance de ShortStory.

+0

Bon essai, même si ce n'est pas tout à fait le faire! Je ne suis pas sûr que ce que j'ai demandé est possible avec le système de type actuel. (Les collections Scala utilisent une énorme pile de traits et très peu de classes instanciées.) –

2

Après discussions sur la liste de diffusion Scala - un grand merci aux gens de m'avoir mis sur la bonne voie! - Je pense que c'est le plus proche que l'on peut arriver à un minimum cadre al. Je laisse ici pour référence, et je suis sur un autre exemple, car il met en lumière ce qui se passe mieux:

abstract class Peano[A,MyType <: Peano[A,MyType]](a: A, f: A=>A) { 
    self: MyType => 
    def newPeano(a: A, f: A=>A): MyType 
    def succ: MyType = newPeano(f(a),f) 
    def count(n: Int): MyType = { 
    if (n<1) this 
    else if (n==1) succ 
    else count(n-1).succ 
    } 
    def value = a 
} 

abstract class Peano2[A,MyType <: Peano2[A,MyType]](a: A, f: A=>A, g: A=>A) extends Peano[A,MyType](a,f) { 
    self: MyType => 
    def newPeano2(a: A, f: A=>A, g: A=>A): MyType 
    def newPeano(a: A, f: A=>A): MyType = newPeano2(a,f,g) 
    def pred: MyType = newPeano2(g(a),f,g) 
    def uncount(n: Int): MyType = { 
    if (n < 1) this 
    else if (n==1) pred 
    else uncount(n-1).pred 
    } 
} 

La clé est l'ajout ici du paramètre de type MyType qui est un espace réservé pour le type de la classe que nous finirons vraiment avec. Chaque fois que nous héritons, nous devons le redéfinir comme un paramètre de type, et nous avons ajouté une méthode constructeur qui créera un nouvel objet de ce type. Si le constructeur change, nous devons créer une nouvelle méthode de constructeur.

Maintenant, quand vous voulez créer une classe à utiliser effectivement, il suffit de remplir la méthode constructeur avec un appel à nouveau (et dire à la classe qu'il est de son propre type):

class Peano2Impl[A](a: A, f: A=>A, g: A=>A) extends Peano2[A,Peano2Impl[A]](a,f,g) { 
    def newPeano2(a: A, f: A=>A, g: A=>A) = new Peano2Impl[A](a,f,g) 
} 

et vous êtes hors et le fonctionnement:

val p = new Peano2Impl(0L , (x:Long)=>x+1 , (y:Long)=>x-1) 

scala> p.succ.value 
res0: Long = 1 

scala> p.pred.value 
res1: Long = -1 

scala> p.count(15).uncount(7).value 
res2: Long = 8 

donc, pour résumer, le minimum passe-partout - si vous voulez inclure des méthodes récursives, qui rompt l'autre style de réponse - est pour toutes les méthodes qui renvoient une nouvelle copie de l'extérieur de la classe (en utilisant de nouvelles ou une usine ou autre) à laisser abstrait (il re, j'ai tout réduit à une méthode qui duplique le constructeur), et vous devez ajouter l'annotation de type MyType comme indiqué. Ensuite, à l'étape finale, ces méthodes de nouvelle copie doivent être instanciées.

Cette stratégie fonctionne également pour la covariance dans A, sauf que cet exemple particulier ne fonctionne pas puisque f et g ne sont pas covariants.

Questions connexes