Qual é a diferença entre auto-tipos e herança de características em Scala?

9

Quando pesquisado no Google, surgem muitas respostas para esse tópico. No entanto, não acho que nenhum deles faça um bom trabalho ilustrando a diferença entre esses dois recursos. Então, eu gostaria de tentar mais uma vez, especificamente ...

O que pode ser feito com tipos próprios e não com herança, e vice-versa?

Para mim, deve haver alguma diferença física quantificável entre os dois, caso contrário eles são apenas nominalmente diferentes.

Se o Traço A estender B ou os tipos próprios B, os dois não ilustram que ser B é um requisito? Onde está a diferença?

Mark Canlas
fonte
Desconfio dos termos que você definiu na recompensa. Por um lado, defina a diferença "física", pois esse é todo o software. Além disso, para qualquer objeto composto criado com mixins, você provavelmente pode criar algo aproximado em função com herança - se você definir a função puramente em termos dos métodos visíveis. Onde eles diferem é em extensibilidade, flexibilidade e composição.
itsbruce
Se você tiver uma variedade de chapas de aço de tamanhos diferentes, poderá prendê-las para formar uma caixa ou soldá-las. De uma perspectiva estreita, isso seria equivalente em funcionalidade - se você ignorar o fato de que uma pode ser facilmente reconfigurada ou ampliada e a outra não. Tenho a sensação de que você argumentará que são equivalentes, embora eu ficaria feliz em provar que estava errado se você dissesse mais sobre seus critérios.
itsbruce
Estou mais do que familiar com o que você está dizendo em geral, mas ainda não entendo qual é a diferença nesse caso específico. Você poderia fornecer alguns exemplos de código que mostram que um método é mais extensível e flexível que o outro? * Código base com extensão * código base com auto tipos * Feature adicionado ao estilo extensão * Feature adicionado ao estilo self tipo
Mark Canlas
OK, acho que posso tentar isso antes da recompensa se esgota;)
itsbruce

Respostas:

11

Se a característica A estender B, a mistura de A fornecerá B precisamente mais o que A acrescenta ou estende. Por outro lado, se o traço A tiver uma auto-referência explicitamente digitada como B, a classe-mãe final também deverá misturar em B ou um tipo descendente de B (e misture-o primeiro , o que é importante).

Essa é a diferença mais importante. No primeiro caso, o tipo preciso de B é cristalizado no ponto A o estende. No segundo, o designer da classe pai decide qual versão de B é usada, no ponto em que a classe pai é composta.

Outra diferença é onde A e B fornecem métodos com o mesmo nome. Onde A estende B, o método de A substitui B. Onde A é misturado após B, o método de A simplesmente vence.

A auto-referência digitada oferece muito mais liberdade; o acoplamento entre A e B está solto.

ATUALIZAR:

Como você não tem certeza sobre o benefício dessas diferenças ...

Se você usa herança direta, cria a característica A que é B + A. Você definiu o relacionamento em pedra.

Se você usar uma auto-referência digitada, qualquer pessoa que queira usar sua característica A na classe C poderá

  • Misture B e depois A em C.
  • Misture um subtipo de B e depois A em C.
  • Misture A em C, onde C é uma subclasse de B.

E esse não é o limite de suas opções, devido ao modo como o Scala permite instanciar uma característica diretamente com um bloco de código como seu construtor.

Quanto à diferença entre a vitória do método de A , porque A é o último da mistura, comparado a A que estende B, considere isso ...

Onde você mescla uma sequência de características, sempre que o método foo()é chamado, o compilador vai para a última característica misturada para procurar e foo(), se não for encontrado, ele percorre a sequência para a esquerda até encontrar uma característica que implemente foo()e use naquela. A também tem a opção de chamar super.foo(), que também percorre a sequência para a esquerda até encontrar uma implementação e assim por diante.

Portanto, se A tem uma auto-referência digitada para B e o escritor de A sabe que B implementa foo(), A pode chamar super.foo()sabendo que, se nada mais fornecer foo(), B fará. No entanto, o criador da classe C tem a opção de remover qualquer outra característica na qual implementa foo(), e A obterá isso.

Novamente, isso é muito mais poderoso e menos limitativo do que A estendendo B e chamando diretamente a versão de B de foo().

itsbruce
fonte
Qual é a diferença funcional entre uma vitória e uma substituição? Estou recebendo A nos dois casos através de mecanismos diferentes? E no seu primeiro exemplo ... No seu primeiro parágrafo, por que não a característica A estende o SuperOfB? Parece que sempre poderíamos remodelar o problema usando qualquer um dos mecanismos. Acho que não estou vendo um caso de uso em que isso não seja possível. Ou estou assumindo muitas coisas.
precisa
Por que você deseja que A estenda uma subclasse de B, se B define o que você precisa? A auto-referência força B (ou uma subclasse) a estar presente, mas dá à escolha do desenvolvedor? Eles podem misturar algo que escreveram depois que você escreveu a característica A, desde que ela se estenda B. Por que restringi-los apenas ao que estava disponível quando você escreveu a característica A?
itsbruce
Atualizado para tornar a diferença super clara.
precisa saber é o seguinte
@itsbruce existe alguma diferença conceitual? IS-A versus HAS-A?
Jas
@Jas No contexto do relacionamento entre os traços A e B , a herança é IS-A, enquanto a auto-referência digitada fornece HAS-A (um relacionamento de composição). Para a classe na qual as características são misturadas, o resultado é IS-A , independentemente.
itsbruce
0

Eu tenho algum código que ilustra algumas das diferenças de visibilidade e implementações "padrão" ao estender vs definir um tipo próprio. Ele não ilustra nenhuma das partes já discutidas sobre como as colisões de nomes reais são resolvidas, mas concentra-se no que é possível e não é possível fazer.

trait A1 {
  self: B =>

  def doit {
    println(bar)
  }
}

trait A2 extends B {
  def doit {
    println(bar)
  }
}

trait B {
  def bar = "default bar"
}

trait BX extends B {
  override def bar = "bar bx"
}

trait BY extends B {
  override def bar = "bar by"
}

object Test extends App {
  // object Thing1 extends A1  // FAIL: does not conform to A1 self-type
  object Thing1 extends A1 with B
  object Thing2 extends A2

  object Thing1X extends A1 with BX
  object Thing1Y extends A1 with BY
  object Thing2X extends A2 with BX
  object Thing2Y extends A2 with BY

  Thing1.doit  // default bar
  Thing2.doit  // default bar
  Thing1X.doit // bar bx
  Thing1Y.doit // bar by
  Thing2X.doit // bar bx
  Thing2Y.doit // bar by

  // up-cast
  val a1: A1 = Thing1Y
  val a2: A2 = Thing2Y

  // println(a1.bar)    // FAIL: not visible
  println(a2.bar)       // bar bx
  // println(a2.bary)   // FAIL: not visible
  println(Thing2Y.bary) // 42
}

Uma diferença importante da IMO é que A1ela não expõe que é necessária Bpara qualquer coisa que apenas a veja A1(como ilustrado nas partes pré-fabricadas). O único código que realmente verá que uma especialização específica Bé usada é o código que sabe explicitamente sobre o tipo composto (como Think*{X,Y}).

Outro ponto é que A2(com extensão) será realmente usado Bse nada for especificado, enquanto A1(o tipo próprio) não diz que será usado, a Bmenos que seja substituído, um B concreto deve ser explicitamente fornecido quando os objetos forem instanciados.

Rouzbeh Delavari
fonte