Qual é a diferença entre auto-tipos e subclasses de características?

387

Um tipo próprio para uma característica A:

trait B
trait A { this: B => }

diz que " Anão pode ser misturado em uma classe concreta que também não se estende B" .

Por outro lado, o seguinte:

trait B
trait A extends B

diz que "qualquer classe (concreta ou abstrata) que se misturar Atambém se misturará em B" .

Essas duas afirmações não significam a mesma coisa? O tipo próprio parece servir apenas para criar a possibilidade de um simples erro em tempo de compilação.

o que estou perdendo?

Dave
fonte
Na verdade, estou interessado aqui nas diferenças entre os tipos de self e a subclasse de características. Eu conheço alguns dos usos comuns para tipos próprios; Eu simplesmente não consigo encontrar uma razão pela qual eles não seriam feitos mais claramente da mesma maneira com a subtipagem.
Dave
32
Pode-se usar parâmetros de tipo dentro de tipos próprios: trait A[Self] {this: Self => }é legal, trait A[Self] extends Selfnão é.
Bluesorblade
3
Um tipo próprio também pode ser uma classe, mas uma característica não pode herdar de uma classe.
cvogt
10
@cvogt: um traço pode herdar de uma classe (pelo menos a partir de 2.10): pastebin.com/zShvr8LX
Erik Kaplun
11
@ Blaisorblade: não é algo que poderia ser resolvido por uma pequena linguagem reprojetada, e não uma limitação fundamental? (pelo menos do ponto de vista da pergunta)
Erik Kaplun

Respostas:

273

É usado predominantemente para injeção de dependência , como no padrão de bolo. Existe um ótimo artigo que cobre muitas formas diferentes de injeção de dependência no Scala, incluindo o Cake Pattern. Se você pesquisar no Google "Cake Pattern and Scala", obterá muitos links, incluindo apresentações e vídeos. Por enquanto, aqui está um link para outra pergunta .

Agora, qual é a diferença entre um tipo de eu e a extensão de uma característica, isso é simples. Se você diz B extends A, então B é um A. Quando você usa auto-tipos, B requer um A. Existem dois requisitos específicos criados com auto-tipos:

  1. Se Bfor estendido, você precisará misturar um A.
  2. Quando uma classe concreta finalmente estende / mescla essas características, alguma classe / característica deve ser implementada A.

Considere os seguintes exemplos:

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 uma subclasse de User, não haveria erro. No código acima, solicitamos um Usersempre que Tweeteré usado, no entanto, um Usernão foi fornecido Wrong, portanto, ocorreu um erro. Agora, com o código acima ainda no escopo, considere:

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

Com Right, o requisito de misturar um Useré atendido. No entanto, o segundo requisito mencionado acima não é atendido: o ônus da implementação Userainda permanece para as classes / características que se estendem Right.

Com RightAgainambos os requisitos são satisfeitos. A Usere uma implementação de Usersão fornecidas.

Para casos de uso mais práticos, consulte os links no início desta resposta! Mas espero que agora você entenda.

Daniel C. Sobral
fonte
3
Obrigado. O padrão de bolo é 90% do que quero dizer por que falo sobre o hype em torno de tipos pessoais ... foi onde vi o tópico pela primeira vez. O exemplo de Jonas Boner é ótimo porque ressalta o ponto da minha pergunta. Se tiver alterado as auto-tipos em seu exemplo aquecedor para ser subtraits então qual seria a diferença (que não seja o erro que você começa quando definir o ComponentRegistry se você não misturar o material certo?
Dave
29
@ Dave: Você quer dizer como trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Isso causaria WarmerComponentImplter essas interfaces. Eles estaria disponível para qualquer coisa que se estendeu WarmerComponentImpl, o que é claramente errado, pois é não um SensorDeviceComponent, nem um OnOffDeviceComponent. Como um tipo próprio, essas dependências estão disponíveis exclusivamente para WarmerComponentImpl. A Listpode ser usado como Arraye vice-versa. Mas eles simplesmente não são a mesma coisa.
Daniel C. Sobral
10
Obrigado Daniel. Esta é provavelmente a principal distinção que eu estava procurando. O problema prático é que o uso de subclassing vazará funcionalidades em sua interface que você não pretende. É o resultado da violação da regra mais teórica "é parte de uma" para traços. Os tipos próprios expressam uma relação "usos-a" entre partes.
Dave
11
@ Rodney Não, não deveria. De fato, usar thiscom tipos próprios é algo que desprezo, pois obscurece, sem uma boa razão, o original this.
Daniel C. Sobral
9
@opensas Experimente self: Dep1 with Dep2 =>.
Daniel C. Sobral
156

Os tipos próprios permitem definir dependências cíclicas. Por exemplo, você pode conseguir isso:

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

A herança usando extendsnão permite isso. Tentar:

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

No livro Odersky, consulte a seção 33.5 (capítulo Criação da interface do usuário da planilha), onde ele menciona:

No exemplo da planilha, a classe Modelo herda do Avaliador e, assim, obtém acesso ao seu método de avaliação. Por outro lado, a classe Evaluator define seu próprio tipo como Model, assim:

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

Espero que isto ajude.

Mushtaq Ahmed
fonte
3
Eu não tinha considerado esse cenário. É o primeiro exemplo de algo que eu vi que não é o mesmo que um tipo pessoal e uma subclasse. No entanto, parece uma espécie de edge-casey e, mais importante, parece uma péssima ideia (eu costumo me esforçar muito para não definir dependências cíclicas!). Você acha que essa é a distinção mais importante?
Dave
4
Acho que sim. Não vejo outra razão pela qual prefira self-types a estender a cláusula. Os tipos de self são detalhados, eles não são herdados (portanto, você deve adicionar tipos de self a todos os subtipos como um ritual) e você só pode ver um membro, mas não pode substituí-lo. Eu estou bem ciente do padrão Cake e de muitos posts mencionando self-types para DI. Mas de alguma forma não estou convencido. Eu havia criado um aplicativo de amostra aqui há muito tempo ( bitbucket.org/mushtaq/scala-di ). Veja especificamente a pasta / src / configs. Consegui o DI para substituir configurações complexas de Spring sem auto-tipos.
Mushtaq Ahmed
Mushtaq, estamos de acordo. Acho que a afirmação de Daniel sobre não expor funcionalidade não intencional é importante, mas, como você coloca, há uma visão espelhada desse 'recurso' ... que você não pode substituir a funcionalidade ou usá-la em subclasses futuras. Isso me diz claramente quando o design exigirá um sobre o outro. Estarei evitando auto-tipos até encontrar uma necessidade genuína - ou seja, se eu começar a usar objetos como módulos, como Daniel aponta. Estou dependências de ligação automática com parâmetros implícitos e um objeto direto de bootstrapper. Eu gosto da simplicidade.
Dave
@ DanielC.Sobral pode ser graças ao seu comentário, mas no momento ele tem mais votos positivos do que o seu anser. Votação positiva para ambos :)
rintcius
Por que não criar apenas uma característica AB? Como os traços A e B devem sempre ser combinados em qualquer classe final, por que separá-los em primeiro lugar?
Rich Oliver
56

Uma diferença adicional é que os tipos próprios podem especificar tipos que não são de classe. Por exemplo

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

O tipo de auto aqui é um tipo estrutural. O efeito é dizer que qualquer coisa que se misture no Foo deve implementar uma unidade de retorno de método sem argumento "fechar". Isso permite mixins seguros para digitação de patos.

Dave Griffith
fonte
41
Na verdade, você também pode usar herança com tipos estruturais: a classe abstrata A se estende {def close: Unit}
Adrian
12
Acho digitação estrutural está usando reflexão, então use somente quando não há outra escolha ...
Eran Medan
@ Adrian, acredito que seu comentário está incorreto. `abstract class A estende {def close: Unit}` é apenas uma classe abstrata com superclasse Object. é apenas a sintaxe permissiva de um Scala para expressões sem sentido. Você pode `a classe X estender {def f = 1}; novo X (). f` por exemplo
Alexey
11
@ Alexey Não vejo por que o seu exemplo (ou o meu) não faz sentido.
Adrian
11
@Adrian, abstract class A extends {def close:Unit}é equivalente a abstract class A {def close:Unit}. Portanto, não envolve tipos estruturais.
Alexey
13

A Seção 2.3 "Anotações de autotipo" do artigo Scala original de Martin Odersky, intitulado Scalable Component Abstractions, na verdade explica muito bem o propósito do autotipo além da composição da mixina: forneça uma maneira alternativa de associar uma classe a um tipo abstrato.

O exemplo dado no artigo foi o seguinte, e não parece ter um correspondente elegante da subclasse:

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;
  }
}
lcn
fonte
Para aqueles que se perguntam por que a subclasse não resolverá isso, a Seção 2.3 também diz o seguinte: “Cada um dos operandos de uma composição de mixina C_0 com ... com C_n deve se referir a uma classe. O mecanismo de composição mixin não permite que C_i se refira a um tipo abstrato. Essa restrição torna possível verificar estaticamente ambigüidades e substituir conflitos no ponto em que uma classe é composta. ”
Luke Maurer
12

Outra coisa que não foi mencionada: como os tipos próprios não fazem parte da hierarquia da classe necessária, eles podem ser excluídos da correspondência de padrões, especialmente quando você está correspondendo exaustivamente a uma hierarquia selada. Isso é conveniente quando você deseja modelar comportamentos ortogonais, como:

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
Bruno Bieth
fonte
10

TL; DR resumo das outras respostas:

  • Os tipos que você estende são expostos a tipos herdados, mas os tipos próprios não são

    por exemplo: class Cow { this: FourStomachs }permite usar métodos disponíveis apenas para ruminantes, como digestGrass. As características que estendem Cow, no entanto, não terão esses privilégios. Por outro lado, class Cow extends FourStomachsirá expor digestGrassa quem quiser extends Cow .

  • auto-tipos permitem dependências cíclicas, estender outros tipos não

jazmit
fonte
9

Vamos começar com a dependência cíclica.

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

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

No entanto, a modularidade dessa solução não é tão boa quanto parece à primeira vista, porque você pode substituir os tipos de self da seguinte maneira:

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

Embora, se você substituir um membro de um tipo próprio, perderá o acesso ao membro original, que ainda poderá ser acessado por meio do super uso da herança. Então, o que realmente é ganho com o uso de herança é:

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

Agora, não posso pretender entender todas as sutilezas do padrão de bolo, mas me parece que o principal método de impor a modularidade é através da composição, e não da herança ou tipos próprios.

A versão da herança é mais curta, mas a principal razão pela qual prefiro a herança sobre os tipos próprios é que acho muito mais complicado obter a ordem de inicialização correta com os tipos próprios. No entanto, existem algumas coisas que você pode fazer com tipos próprios que você não pode fazer com herança. Auto-tipos podem usar um tipo, enquanto a herança exige uma característica ou uma classe, como em:

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

Você pode até fazer:

trait TypeBuster
{ this: Int with String => }

Embora você nunca seja capaz de instanciar isso. Não vejo nenhuma razão absoluta para não ser capaz de herdar de um tipo, mas certamente acho que seria útil ter classes e características de construtor de caminho, como temos características / classes de construtor de tipo. Infelizmente

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

Nós temos isso:

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 }

Ou isto:

  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 }

Um ponto que deve ser mais enfatizado é que os traços podem estender as classes. Agradecemos a David Maclver por apontar isso. Aqui está um exemplo do meu próprio código:

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]

ScnBaseherda da classe Swing Frame, para que possa ser usada como um tipo próprio e depois misturada no final (na instanciação). No entanto, val geomRprecisa ser inicializado antes de ser usado pela herança de características. Portanto, precisamos de uma classe para impor a inicialização prévia de geomR. A classe ScnVistapode ser herdada de várias características ortogonais das quais elas próprias podem ser herdadas. O uso de vários parâmetros de tipo (genéricos) oferece uma forma alternativa de modularidade.

Rich Oliver
fonte
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
}
Oleg Galako
fonte
4

Um tipo próprio permite especificar quais tipos têm permissão para misturar uma característica. Por exemplo, se você tem uma característica com um tipo próprio Closeable, essa característica sabe que as únicas coisas que podem misturá-la devem implementar a Closeableinterface.

kikibobo
fonte
3
@ Blaisorblade: Gostaria de saber se você pode ter interpretado mal a resposta do kikibobo - o tipo de característica de uma característica permite que você restrinja os tipos que a misturam, e isso faz parte da sua utilidade. Por exemplo, se definirmos trait A { self:B => ... }, uma declaração X with Asó será válida se X estender B. Sim, você pode dizer X with A with Q, onde Q não estende B, mas acredito que o ponto de kikibobo era que X é tão restrito. Ou eu perdi alguma coisa?
AmigoNico
11
Obrigado, você está certo. Meu voto foi bloqueado, mas felizmente pude editar a resposta e depois mudar de voto.
Bluesorblade
1

Atualização: A principal diferença é que os tipos próprios podem depender de várias classes (eu admito que isso seja um pouco esquecido). Por exemplo, você pode ter

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

Isso permite adicionar o Employeemixin a qualquer coisa que seja uma subclasse de Persone Expense. Obviamente, isso só tem sentido se for Expenseestendido Personou vice-versa. O ponto é que o uso de tipos próprios Employeepode ser independente da hierarquia das classes das quais depende. Ele não se importa com o que estende o quê - Se você alternar a hierarquia de Expensevs Person, não precisará modificar Employee.

Petr Pudlák
fonte
O Funcionário não precisa ser de classe para descender de Pessoa. Traços podem estender classes. Se a característica do funcionário estender Pessoa, em vez de usar um tipo próprio, o exemplo ainda funcionaria. Acho o seu exemplo interessante, mas não parece ilustrar um caso de uso para tipos próprios.
Morgan Creighton
@MorganCreighton Justo, eu não sabia que os traços podem estender as aulas. Vou pensar se encontrar um exemplo melhor.
Petr Pudlák
Sim, é um recurso de linguagem surpreendente. Se a característica Employee estendeu a classe Person, então qualquer classe "inedida" em Employee também teria que estender Person. Mas essa restrição ainda está presente se Employee usou um tipo de self em vez de estender Person. Saúde, Petr!
Morgan Creighton
11
Não vejo por que "isso só é significativo se a Despesa estender a Pessoa ou vice-versa".
Robin Green
0

no primeiro caso, uma sub-característica ou sub-classe de B pode ser misturada a qualquer que seja o uso de A. Portanto, B pode ser uma característica abstrata.

IttayD
fonte
Não, B pode ser (e de fato é) um "traço abstrato" nos dois casos. Portanto, não há diferença dessa perspectiva.
Robin Green