Qual è la differenza tra le sottoclassi dei tipi di sé e dei tratti?


387

Un tipo di auto per un tratto A:

trait B
trait A { this: B => }

afferma che " Anon può essere mischiato in una classe concreta che non si estende anche B" .

D'altra parte, il seguente:

trait B
trait A extends B

dice che "qualsiasi classe (concreta o astratta) che si mescola Asi mescolerà anche in B" .

Queste due affermazioni non significano la stessa cosa? L'autotipo sembra servire solo a creare la possibilità di un semplice errore di compilazione.

Cosa mi sto perdendo?


In realtà sono interessato qui alle differenze tra tipi di sé e sottoclasse nei tratti. Conosco alcuni degli usi comuni per i tipi di sé; Non riesco proprio a trovare un motivo per cui non si farebbero più chiaramente allo stesso modo con il sottotipo.
Dave,

32
Si possono usare i parametri di tipo all'interno dei tipi di sé: trait A[Self] {this: Self => }è legale, trait A[Self] extends Selfnon lo è.
Blaisorblade,

3
Un tipo di sé può anche essere una classe, ma un tratto non può ereditare da una classe.
cvogt,

10
@cvogt: un tratto può ereditare da una classe (almeno a partire dal 2.10): pastebin.com/zShvr8LX
Erik Kaplun

1
@Blaisorblade: non è qualcosa che potrebbe essere risolto da una riprogettazione del linguaggio di piccole dimensioni, e non una limitazione fondamentale? (almeno dal punto di vista della domanda)
Erik Kaplun,

Risposte:


273

È utilizzato principalmente per l' iniezione di dipendenza , come nel modello a torta. Esiste un grande articolo che copre molte diverse forme di iniezione di dipendenza in Scala, incluso il modello di torta. Se utilizzi Google "Cake Pattern and Scala", otterrai molti link, tra cui presentazioni e video. Per ora, ecco un link ad un'altra domanda .

Ora, per quanto riguarda la differenza tra un tipo di sé e l'estensione di un tratto, è semplice. Se dici B extends A, allora B è un A. Quando si usano i tipi di sé, è B necessario un A. Esistono due requisiti specifici che vengono creati con i tipi automatici:

  1. Se Bè esteso, allora stai richiesto di mescolare-in A.
  2. Quando una classe concreta alla fine estende / mescola questi tratti, alcune classi / tratti devono implementarsi A.

Considera i seguenti esempi:

scala> trait User { def name: String }
defined trait User

scala> trait Tweeter {
     |   user: User =>
     |   def tweet(msg: String) = println(s"$name: $msg")
     | }
defined trait Tweeter

scala> trait Wrong extends Tweeter {
     |   def noCanDo = name
     | }
<console>:9: error: illegal inheritance;
 self-type Wrong does not conform to Tweeter's selftype Tweeter with User
       trait Wrong extends Tweeter {
                           ^
<console>:10: error: not found: value name
         def noCanDo = name
                       ^

Se Tweeterfosse una sottoclasse di User, non ci sarebbero errori. Nel codice sopra, abbiamo richiesto un Userquando Tweeterviene utilizzato, tuttavia un Usernon è stato fornito a Wrong, così abbiamo ottenuto un errore. Ora, con il codice sopra ancora nell'ambito, considera:

scala> trait DummyUser extends User {
     |   override def name: String = "foo"
     | }
defined trait DummyUser

scala> trait Right extends Tweeter with User {
     |   val canDo = name
     | }
defined trait Right 

scala> trait RightAgain extends Tweeter with DummyUser {
     |   val canDo = name
     | }
defined trait RightAgain

Con Right, il requisito di mescolare a Userè soddisfatto. Tuttavia, il secondo requisito di cui sopra non è soddisfatto: l'onere dell'attuazione Userrimane ancora per le classi / tratti che si estendono Right.

Con RightAgainentrambi i requisiti sono soddisfatti. A Usere un'implementazione di Usersono forniti.

Per casi d'uso più pratici, consultare i link all'inizio di questa risposta! Ma spero che ora lo capisca.


3
Grazie. Il modello di torta è il 90% di ciò che intendo perché parlo dell'hype attorno ai tipi di sé ... è dove ho visto l'argomento per la prima volta. L'esempio di Jonas Boner è ottimo perché sottolinea il punto della mia domanda. Se cambiassi i tipi di sé nell'esempio del suo riscaldatore in sottotitoli, quale sarebbe la differenza (oltre all'errore che si ottiene quando si definisce il ComponentRegistry se non si mescolano le cose giuste?
Dave

29
@Dave: vuoi dire trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Ciò provocherebbe WarmerComponentImplquelle interfacce. Sarebbero disponibili a tutto ciò che ha esteso WarmerComponentImpl, che è chiaramente sbagliato, come è non è una SensorDeviceComponent, né una OnOffDeviceComponent. Come auto-tipo, queste dipendenze sono disponibili esclusivamente per WarmerComponentImpl. A Listpotrebbe essere usato come an Arraye viceversa. Ma non sono la stessa cosa.
Daniel C. Sobral,

10
Grazie Daniel. Questa è probabilmente la principale distinzione che stavo cercando. Il problema pratico è che l'utilizzo della sottoclasse perderà funzionalità nell'interfaccia che non si intende. È il risultato della violazione della regola più teorica "fa parte di una" per i tratti. I tipi di sé esprimono una relazione "usi-a" tra le parti.
Dave,

11
@Rodney No, non dovrebbe. In effetti, l'utilizzo thiscon i tipi di sé è qualcosa su cui guardo in basso, poiché ombreggia senza motivo l'originale this.
Daniel C. Sobral,

9
@opensas Try self: Dep1 with Dep2 =>.
Daniel C. Sobral,

156

I tipi di sé consentono di definire dipendenze cicliche. Ad esempio, puoi ottenere questo:

trait A { self: B => }
trait B { self: A => }

L'uso dell'ereditarietà extendsnon lo consente. Provare:

trait A extends B
trait B extends A
error:  illegal cyclic reference involving trait A

Nel libro Odersky, guarda la sezione 33.5 (Creazione del capitolo UI del foglio di calcolo) in cui menziona:

Nell'esempio di foglio di calcolo, il modello di classe eredita da Valutatore e quindi ottiene l'accesso al suo metodo di valutazione. Per andare dall'altra parte, Class Evaluator definisce il proprio tipo di auto come Modello, in questo modo:

package org.stairwaybook.scells
trait Evaluator { this: Model => ...

Spero che sia di aiuto.


3
Non avevo considerato questo scenario. È il primo esempio di qualcosa che ho visto che non è lo stesso di un tipo di sé come in una sottoclasse. Tuttavia, sembra una specie di edge-casey e, cosa più importante, sembra una cattiva idea (di solito faccio di tutto per NON definire dipendenze cicliche!). Trovi che questa sia la distinzione più importante?
Dave,

4
Credo di si. Non vedo nessun altro motivo per cui preferirei i tipi di sé all'estensione della clausola. I tipi di sé sono prolissi, non vengono ereditati (quindi devi aggiungere i tipi di sé a tutti i sottotipi come rituale) e puoi vedere solo i membri ma non puoi ignorarli. Sono ben consapevole del modello di torta e di molti post che menzionano i tipi di sé per DI. Ma in qualche modo non sono convinto. Avevo creato un'app di esempio qui da molto tempo ( bitbucket.org/mushtaq/scala-di ). Guarda in particolare la cartella / src / configs. Ho raggiunto il DI per sostituire complesse configurazioni a molla senza auto-tipi.
Mushtaq Ahmed,

Mushtaq, siamo d'accordo. Penso che l'affermazione di Daniel sul non esporre la funzionalità involontaria sia importante ma, come dici tu, c'è una visione speculare di questa "caratteristica" ... che non puoi scavalcare la funzionalità o usarla in future sottoclassi. Questo mi dice chiaramente quando il design richiederà l'uno sull'altro. Eviterò i tipi di sé fino a quando non avrò un bisogno reale, vale a dire se comincio a usare oggetti come moduli, come sottolinea Daniel. Sto autowiring le dipendenze con parametri impliciti e un semplice oggetto bootstrapper. Mi piace la semplicità.
Dave,

@ DanielC.Sobral potrebbe essere grazie al tuo commento, ma al momento ha più voti della tua risposta. Valorizzando entrambi :)
Rintcius

Perché non creare solo un tratto AB? Poiché i tratti A e B devono sempre essere combinati in qualsiasi classe finale, perché separarli in primo luogo?
Rich Oliver,

56

Un'ulteriore differenza è che i tipi di sé possono specificare tipi non di classe. Per esempio

trait Foo{
   this: { def close:Unit} => 
   ...
}

L'auto-tipo qui è di tipo strutturale. L'effetto è di dire che tutto ciò che si mescola in Foo deve implementare un'unità di ritorno del metodo "close" senza arg. Ciò consente mixin sicuri per la dattilografia.


41
In realtà puoi usare l'ereditarietà anche con tipi strutturali: la classe astratta A si estende {def close: Unit}
Adrian

12
Penso che la digitazione strutturale stia usando la riflessione, quindi usala solo quando non c'è altra scelta ...
Eran Medan,

@Adrian, credo che il tuo commento sia errato. `abstract class A extends {def close: Unit}` è solo una classe astratta con superclasse di oggetti. è solo la sintassi permissiva di Scala alle espressioni senza senso. Puoi `estendere la classe X {def f = 1}; nuova X (). f` per esempio
Alexey,

1
@Alexey Non vedo perché il tuo esempio (o il mio) sia privo di senso.
Adrian,

1
@Adrian, abstract class A extends {def close:Unit}è equivalente a abstract class A {def close:Unit}. Quindi non coinvolge tipi strutturali.
Alexey,

13

La sezione 2.3 "Annotazioni di tipo di sé" delle astrazioni dei componenti scalabili della scala originale di Martin Odersky spiega in realtà molto bene lo scopo del tipo di auto oltre la composizione del mixin: fornire un modo alternativo di associare una classe a un tipo astratto.

L'esempio fornito nel documento era simile al seguente e non sembra avere un corrispondente corrispondente in una sottoclasse:

abstract class Graph {
  type Node <: BaseNode;
  class BaseNode {
    self: Node =>
    def connectWith(n: Node): Edge =
      new Edge(self, n);
  }
  class Edge(from: Node, to: Node) {
    def source() = from;
    def target() = to;
  }
}

class LabeledGraph extends Graph {
  class Node(label: String) extends BaseNode {
    def getLabel: String = label;
    def self: Node = this;
  }
}

Per coloro che si chiedono perché la sottoclasse non risolva questo problema, la Sezione 2.3 dice anche questo: “Ciascuno degli operandi di una composizione di mixin C_0 con ... con C_n, deve fare riferimento a una classe. Il meccanismo di composizione del mixin non consente a C_i di fare riferimento a un tipo astratto. Questa limitazione consente di verificare staticamente le ambiguità e di superare i conflitti nel punto in cui è composta una classe. "
Luke Maurer,

12

Un'altra cosa che non è stata menzionata: poiché i tipi di sé non fanno parte della gerarchia della classe richiesta, possono essere esclusi dalla corrispondenza dei modelli, specialmente quando si esegue una corrispondenza esaustiva con una gerarchia sigillata. Ciò è utile quando si desidera modellare comportamenti ortogonali come:

sealed trait Person
trait Student extends Person
trait Teacher extends Person
trait Adult { this : Person => } // orthogonal to its condition

val p : Person = new Student {}
p match {
  case s : Student => println("a student")
  case t : Teacher => println("a teacher")
} // that's it we're exhaustive

10

TL; riassunto DR delle altre risposte:

  • I tipi che estendi sono esposti ai tipi ereditati, ma i tipi di sé non lo sono

    ad esempio: class Cow { this: FourStomachs }consente di utilizzare metodi disponibili solo per i ruminanti, come ad esempio digestGrass. I tratti che estendono Cow non avranno tali privilegi. D'altra parte, class Cow extends FourStomachssarà esposto digestGrassa chiunque extends Cow .

  • i tipi di sé consentono dipendenze cicliche, mentre l'estensione di altri tipi no


9

Cominciamo con la dipendenza ciclica.

trait A {
  selfA: B =>
  def fa: Int }

trait B {
  selfB: A =>
  def fb: String }

Tuttavia, la modularità di questa soluzione non è eccezionale come potrebbe sembrare, poiché è possibile ignorare i tipi di sé in questo modo:

trait A1 extends A {
  selfA1: B =>
  override def fb = "B's String" }
trait B1 extends B {
  selfB1: A =>
  override def fa = "A's String" }
val myObj = new A1 with B1

Tuttavia, se si ignora un membro di un tipo di sé, si perde l'accesso al membro originale, a cui è ancora possibile accedere tramite l'ereditarietà super. Quindi ciò che si guadagna davvero usando l'eredità è:

trait AB {
  def fa: String
  def fb: String }
trait A1 extends AB
{ override def fa = "A's String" }        
trait B1 extends AB
{ override def fb = "B's String" }    
val myObj = new A1 with B1

Ora non posso pretendere di comprendere tutte le sottigliezze del modello di torta, ma mi sembra che il metodo principale per applicare la modularità sia attraverso la composizione piuttosto che l'ereditarietà o i tipi di sé.

La versione dell'ereditarietà è più breve, ma il motivo principale per cui preferisco l'ereditarietà rispetto ai tipi di sé è che trovo molto più difficile ottenere l'ordine di inizializzazione corretto con i tipi di sé. Tuttavia, ci sono alcune cose che puoi fare con i tipi di sé che non puoi fare con l'eredità. I tipi di sé possono usare un tipo mentre l'ereditarietà richiede un tratto o una classe come in:

trait Outer
{ type T1 }     
trait S1
{ selfS1: Outer#T1 => } //Not possible with inheritance.

Puoi anche fare:

trait TypeBuster
{ this: Int with String => }

Anche se non sarai mai in grado di istanziarlo. Non vedo alcun motivo assoluto per non essere in grado di ereditare da un tipo, ma certamente penso che sarebbe utile avere classi e tratti del costruttore di percorsi dato che abbiamo tratti / classi del costruttore di tipi. Sfortunatamente

trait InnerA extends Outer#Inner //Doesn't compile

Abbiamo questo:

trait Outer
{ trait Inner }
trait OuterA extends Outer
{ trait InnerA extends Inner }
trait OuterB extends Outer
{ trait InnerB extends Inner }
trait OuterFinal extends OuterA with OuterB
{ val myV = new InnerA with InnerB }

O questo:

  trait Outer
  { trait Inner }     
  trait InnerA
  {this: Outer#Inner =>}
  trait InnerB
  {this: Outer#Inner =>}
  trait OuterFinal extends Outer
  { val myVal = new InnerA with InnerB with Inner }

Un punto che dovrebbe essere più enfatizzato è che i tratti possono estendere le classi. Grazie a David Maclver per averlo segnalato. Ecco un esempio dal mio codice:

class ScnBase extends Frame
abstract class ScnVista[GT <: GeomBase[_ <: TypesD]](geomRI: GT) extends ScnBase with DescripHolder[GT] )
{ val geomR = geomRI }    
trait EditScn[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
trait ScnVistaCyl[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]

ScnBaseeredita dalla classe Swing Frame, quindi potrebbe essere usato come un tipo di auto e poi miscelato alla fine (all'istanza). Tuttavia, val geomRdeve essere inizializzato prima di essere utilizzato ereditando i tratti. Quindi abbiamo bisogno di una classe per far rispettare la precedente inizializzazione di geomR. La classe ScnVistapuò quindi essere ereditata da più tratti ortogonali che possono essere ereditati da loro stessi. L'uso di più parametri di tipo (generici) offre una forma alternativa di modularità.


7
trait A { def x = 1 }
trait B extends A { override def x = super.x * 5 }
trait C1 extends B { override def x = 2 }
trait C2 extends A { this: B => override def x = 2}

// 1.
println((new C1 with B).x) // 2
println((new C2 with B).x) // 10

// 2.
trait X {
  type SomeA <: A
  trait Inner1 { this: SomeA => } // compiles ok
  trait Inner2 extends SomeA {} // doesn't compile
}

4

Un tipo di sé consente di specificare quali tipi sono autorizzati a mescolare un tratto. Ad esempio, se hai un tratto con un tipo di auto Closeable, quel tratto sa che le uniche cose a cui è permesso mescolarlo devono implementare l' Closeableinterfaccia.


3
@Blaisorblade: Mi chiedo se potresti avere frainteso la risposta di kikibobo - il tipo di sé di un tratto ti consente davvero di vincolare i tipi che possono mescolarlo e che fa parte della sua utilità. Ad esempio, se definiamo che trait A { self:B => ... }una dichiarazione X with Aè valida solo se X estende B. Sì, puoi dire X with A with Q, dove Q non estende B, ma credo che il punto di kikibobo sia che X sia così vincolato. O mi sono perso qualcosa?
AmigoNico,

1
Grazie hai ragione. Il mio voto è stato bloccato, ma per fortuna ho potuto modificare la risposta e quindi cambiare il mio voto.
Blaisorblade,

1

Aggiornamento: una differenza principale è che i tipi di sé possono dipendere da più classi (ammetto che è un caso un po 'angolare). Ad esempio, puoi avere

class Person {
  //...
  def name: String = "...";
}

class Expense {
  def cost: Int = 123;
}

trait Employee {
  this: Person with Expense =>
  // ...

  def roomNo: Int;

  def officeLabel: String = name + "/" + roomNo;
}

Ciò consente di aggiungere il Employeemixin solo a tutto ciò che è una sottoclasse di Persone Expense. Naturalmente, questo ha senso solo se si Expenseestende Persono viceversa. Il punto è che l'uso dei tipi di sé Employeepuò essere indipendente dalla gerarchia delle classi da cui dipende. Non importa cosa estende cosa - Se cambi la gerarchia di Expensevs Person, non devi modificare Employee.


Il dipendente non deve essere una classe per discendere dalla persona. I tratti possono estendere le classi. Se il tratto Dipendente estendesse Persona invece di utilizzare un tipo di auto, l'esempio continuerebbe a funzionare. Trovo il tuo esempio interessante, ma non sembra illustrare un caso d'uso per i tipi di sé.
Morgan Creighton,

@MorganCreighton Abbastanza giusto, non sapevo che i tratti possono estendere le classi. Ci penserò se riesco a trovare un esempio migliore.
Petr Pudlák,

Sì, è una caratteristica del linguaggio sorprendente. Se il tratto Employee ha esteso la persona di classe, allora qualsiasi classe alla fine "trattenuta in" Employee dovrebbe anche estendere la persona. Ma tale restrizione è ancora presente se il Dipendente utilizzava un tipo di auto anziché estendere la Persona. Saluti, Petr!
Morgan Creighton,

1
Non vedo perché "questo ha senso solo se Expense estende la Persona o viceversa".
Robin Green,

0

nel primo caso, un sotto-tratto o sotto-classe di B può essere mischiato a qualunque usi A. Quindi B può essere un tratto astratto.


No, B può essere (ed effettivamente è) un "tratto astratto" in entrambi i casi. Quindi non c'è differenza da quella prospettiva.
Robin Green,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.