15

Załóżmy, że chcemy zbudować nową klasę generyczną, Novel[A]. Ta klasa będzie zawierała wiele użytecznych metod - być może jest to typ kolekcji - i dlatego chcesz ją podklasować. Ale chcesz, aby metody zwracały typ podklasy, a nie typ oryginalny. W Scala 2.8, jaka jest minimalna ilość pracy, którą należy wykonać, aby metody tej klasy zwróciły odpowiednią podklasę, a nie oryginał? Na przykład,Minimalne ramy w Scali dla kolekcji z odziedziczonym typem zwrotu

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] 

Czy to minimalną zmianę kwoty, jeżeli chcesz Novel być kowariantna?

(2.8 kolekcje robią to między innymi, ale grają też z typami powrotu w bardziej fantazyjny (i użyteczny) sposób - pytanie brzmi, jak mało ramy można uciec, jeśli chce się tylko tych podtypów - zawsze -return-podtypów.)

Edytuj: Założono w powyższym kodzie, że reverse tworzy kopię. Jeśli dokonasz modyfikacji w miejscu, a następnie zwrócisz, możesz użyć this.type, ale to nie działa, ponieważ kopia nie jest this.

Arjan powiązane z innym pytaniem, które proponuje następujące rozwiązanie:

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

który zasadniczo polega na systemie typu, aby dostać to, co chcemy. Ale tak naprawdę nie jest to rozwiązanie, ponieważ skoro już okłamywaliśmy system typów, kompilator nie może nam pomóc, abyśmy naprawdę odzyskali do odzyskania wartości ShortStory, kiedy myślimy, że to robimy. (Na przykład, nie będziemy musieli przesłonić reverse w powyższym przykładzie, aby kompilator był szczęśliwy, ale nasze typy nie byłyby tym, czego chcieliśmy.)

+3

Prawdopodobnie nie miałeś na myśli typów Singleton Scali jako przykładu minimalnej struktury, którą miałeś na myśli, jak zakładam? W każdym przypadku przykłady online pokazujące, jak można je wykorzystać, można znaleźć na stronie http://scalada.blogspot.com/2008/02/thistype-for-chaining-method-calls.html. Jest także przykład na SO, który pokazuje, jak można produkować niezmienne klasy za pomocą jakiegoś hackery http://stackoverflow.com/questions/775312/why-cannot-this-type-be-used-for-new-instances –

+0

@Arjan - Nie miałem na myśli tego, ponieważ to zbyt ograniczające. (Kopiowanie metod jest zabronione, na przykład.) –

Odpowiedz

3

nie myślałem przez to w pełni, ale wpisywane sprawdza:

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

Wersja wspólnie wariant może być dużo ładniejszy:

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

To wygląda obiecująco, ale co, jeśli chcę podklasować "ShortStory"? Problem polega na tym, że mógłbym mieć użyteczny kod inicjalizacyjny w jednej klasie i chcieć go podklasować bez utraty tej pracy - więc wolałbym, aby wszystko nie pochodziło z cech. Jednakże, jeśli odpowiedź brzmi, że nie można uzyskać systemu typu, który zgodziłby się na moje żądania, wygląda to na rozsądną nagrodę pocieszenia. –

5

Edycja: Właśnie zdałem sobie sprawę, że Rex ma konkretną klasę Powieść w jego przykładzie nie jest to cecha, której użyłem poniżej. Implementacja cechy jest zbyt prosta, aby być rozwiązaniem problemu Rexa. Można to zrobić również przy użyciu konkretnej klasy (patrz niżej), ale jedynym sposobem, w jaki mógłbym wykonać tę pracę, jest użycie niektórych rzutów, co sprawia, że ​​nie jest to naprawdę "bezpieczny czas na kompilację". To więc nie kwalifikuje się jako rozwiązanie.

nie

Być może najładniejszy, ale prosty przykład przy użyciu abstrakcyjnych typów członków mogą być realizowane w następujący sposób:


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 

To na pewno minimalne, ale wątpię, czy to byłoby na tyle być używane jako rodzaj ramy. I oczywiście typy nie powróciły tak blisko, jak w przypadku struktury Scala, więc może to być może trochę zbyt proste. Jednak dla danego przypadku wydaje się, że wykonuje on swoją pracę. Jak zauważono powyżej, nie jest to zadanie dla danego przypadku, więc potrzebne jest tutaj inne rozwiązanie.

Yet Another Edit: Coś podobnego można zrobić przy użyciu betonu klasy, jak również, że choć nie wystarczy być także bezpieczny typ:


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

I kod będzie działać jak w przykładzie cecha. Ale cierpi na ten sam problem, co Rex w swojej edycji. Zastąpienie w ShortStory nie jest konieczne, aby ta kompilacja została wykonana. Jeśli jednak tego nie zrobisz, zakończy się niepowodzeniem w środowisku wykonawczym i wywoła metodę odwrotną w instancji ShortStory.

+0

Dobra próba, nawet jeśli to się nie udało! Nie jestem pewien, czy to, o co prosiłem, jest możliwe w obecnym systemie typów. (Kolekcje Scala używają ogromnej ilości cech i bardzo niewielu utworzonych klas.) –

2

Po dyskusje na liście mailingowej Scala - wielkie podziękowania dla ludzi za ustawienie mnie na właściwej drodze! - Myślę, że jest to najbliższe, że można dojść do minimum al ram. Zostawię go tutaj dla odniesienia, i używam inny przykład dlatego, że podkreśla to, co dzieje się na lepsze:

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

Kluczem tutaj jest dodanie parametru MyType typu, które jest symbolem zastępczym dla danego typu klasa, którą naprawdę otrzymamy. Za każdym razem, gdy dziedziczymy, musimy przedefiniować go jako parametr typu, a my dodamy metodę konstruktora, która utworzy nowy obiekt tego typu. Jeśli konstruktor się zmieni, musimy stworzyć nową metodę konstruktora.

Teraz, gdy chcemy utworzyć klasę, czy korzystać, trzeba tylko wypełnić metody konstruktora z wezwaniem do nowych (i powiedzieć uczniom, że jest to jego własny typ):

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

i ruszasz i prowadzenie:

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 

Tak, aby przejrzeć, minimalny boilerplate - jeśli chcesz obejmują metody rekurencyjne, które łamie inny styl odpowiedzią - jest dla wszystkich metod, które zwracają nowy kopia spoza klasy (przy użyciu nowej lub fabryki lub cokolwiek innego) pozostanie abstrakcyjna (he re, dopracowałem wszystko do jednej metody, która powiela konstruktor) i musisz dodać adnotację typu MyType, jak pokazano. Następnie, na ostatnim etapie, konieczne jest utworzenie instancji tych metod.

Ta strategia działa również w przypadku kowariancji pod numerem A, z tym że ten konkretny przykład nie działa, ponieważ f i g nie są kowariantne.