Scala: Tipos abstratos vs genéricos

250

Eu estava lendo A Tour of Scala: Abstract Types . Quando é melhor usar tipos abstratos?

Por exemplo,

abstract class Buffer {
  type T
  val element: T
}

em vez disso, genéricos, por exemplo,

abstract class Buffer[T] {
  val element: T
}
thatismatt
fonte

Respostas:

257

Você tem um bom ponto de vista sobre esse assunto aqui:

O objetivo do sistema tipo Scala de
conversar com Martin Odersky, parte III
de Bill Venners e Frank Sommers (18 de maio de 2009)

Atualização (outubro de 2009): o que segue abaixo foi realmente ilustrado neste novo artigo por Bill Venners:
Membros de tipo abstrato versus parâmetros de tipo genérico no Scala (consulte o resumo no final)


(Aqui está o extrato relevante da primeira entrevista, maio de 2009, ênfase minha)

Princípio geral

Sempre houve duas noções de abstração:

  • parametrização e
  • membros abstratos.

Em Java, você também possui os dois, mas isso depende do que você está abstraindo.
Em Java, você tem métodos abstratos, mas não pode passar um método como parâmetro.
Você não possui campos abstratos, mas pode passar um valor como parâmetro.
Da mesma forma, você não possui membros do tipo abstrato, mas pode especificar um tipo como parâmetro.
Portanto, em Java, você também tem os três, mas há uma distinção sobre qual princípio de abstração você pode usar para que tipos de coisas. E você poderia argumentar que essa distinção é bastante arbitrária.

O Caminho Scala

Decidimos ter os mesmos princípios de construção para todos os três tipos de membros .
Assim, você pode ter campos abstratos e parâmetros de valor.
Você pode passar métodos (ou "funções") como parâmetros ou abstraí-los.
Você pode especificar tipos como parâmetros ou abstraí-los.
E o que obtemos conceitualmente é que podemos modelar um em termos do outro. Pelo menos em princípio, podemos expressar todo tipo de parametrização como uma forma de abstração orientada a objetos. Então, em certo sentido, você poderia dizer que Scala é uma linguagem mais ortogonal e completa.

Por quê?

O que, em particular, os tipos abstratos compram é um bom tratamento para esses problemas de covariância de que falamos antes.
Um problema padrão, que existe há muito tempo, é o problema de animais e alimentos.
O quebra-cabeça era ter uma aula Animalcom um método eat, que come alguma comida.
O problema é que, se subclassificarmos Animal e tivermos uma classe como Cow, eles comerão apenas capim e não alimentos arbitrários. Uma vaca não pode comer um peixe, por exemplo.
O que você quer é poder dizer que uma vaca tem um método de comer que come apenas capim e não outras coisas.
Na verdade, você não pode fazer isso em Java porque acontece que você pode construir situações desagradáveis, como o problema de atribuir um Fruit a uma variável da Apple da qual falei anteriormente.

A resposta é que você adiciona um tipo abstrato na classe Animal .
Você diz, minha nova classe Animal tem um tipo de SuitableFoodqual eu não conheço.
Portanto, é um tipo abstrato. Você não fornece uma implementação do tipo. Então você tem um eatmétodo que come apenas SuitableFood.
E então na Cowclasse eu dizia: OK, eu tenho uma vaca, que estende a classe Animale para Cow type SuitableFood equals Grass.
Portanto, tipos abstratos fornecem essa noção de tipo em uma superclasse que eu não conheço, que depois preencho posteriormente nas subclasses com algo que eu sei .

Mesmo com parametrização?

De fato você pode. Você pode parametrizar a classe Animal com o tipo de alimento que ele come.
Mas, na prática, quando você faz isso com muitas coisas diferentes, isso leva a uma explosão de parâmetros e, geralmente, além do mais, em limites de parâmetros .
No ECOOP de 1998, Kim Bruce, Phil Wadler e eu tínhamos um artigo onde mostramos que, à medida que você aumenta o número de coisas que não sabe, o programa típico crescerá quadraticamente .
Portanto, existem boas razões para não fazer parâmetros, mas ter esses membros abstratos, porque eles não causam essa explosão quadrática.


thatismatt pergunta nos comentários:

Você acha que o seguinte é um resumo justo:

  • Resumo Os tipos são usados ​​nos relacionamentos 'has-a' ou 'uses-a' (por exemplo, a Cow eats Grass)
  • onde os genéricos geralmente são relacionamentos 'de' (por exemplo List of Ints)

Não sei se o relacionamento é tão diferente entre o uso de tipos abstratos ou genéricos. O que é diferente é:

  • como eles são usados ​​e
  • como os limites dos parâmetros são gerenciados.

Para entender o que Martin está falando quando se trata de "explosão de parâmetros e, geralmente, o que é mais, dentro dos limites ", e seu subsequente crescimento quadraticamente quando o tipo abstrato é modelado usando genéricos, você pode considerar o artigo " Abstração de Componente Escalável "escrito por ... Martin Odersky e Matthias Zenger para OOPSLA 2005, referenciados nas publicações do projeto Palcom (concluído em 2007).

Extratos relevantes

Definição

Os membros do tipo resumo fornecem uma maneira flexível de abstrair sobre tipos concretos de componentes.
Os tipos abstratos podem ocultar informações sobre as partes internas de um componente, semelhante ao uso em assinaturas SML . Em uma estrutura orientada a objetos em que as classes podem ser estendidas por herança, elas também podem ser usadas como um meio flexível de parametrização (geralmente chamado de polimorfismo da família, consulte, por exemplo , esta entrada do blog e o artigo de Eric Ernst ).

(Nota: O polimorfismo familiar foi proposto para linguagens orientadas a objetos como uma solução para dar suporte a classes recursivas mutuamente reutilizáveis ​​e seguras ao tipo.
Uma idéia-chave do polimorfismo familiar é a noção de famílias, usada para agrupar classes recursivas mutuamente)

abstração de tipo limitado

abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}

Aqui, a declaração de tipo de T é restringida por um limite de tipo superior que consiste em um nome de classe Ordenado e um refinamento { type O = T }.
Os restringe limite superior das especializações de T em subclasses para os subtipos de A a Z para a qual o membro tipo Ode equals T.
Devido a essa restrição, <é garantido que o método da classe Ordered seja aplicável a um receptor e a um argumento do tipo T.
O exemplo mostra que o membro do tipo limitado pode aparecer como parte do limite.
(ou seja, Scala suporta polimorfismo limitado a F )

(Nota, do artigo de Peter Canning, William Cook, Walter Hill, Walter Olthoff: A
quantificação limitada foi introduzida por Cardelli e Wegner como um meio de digitar funções que operam uniformemente em todos os subtipos de um determinado tipo.
Eles definiram um modelo simples de "objeto" e usado quantificação limitada ao tipo-verificação funções que fazem sentido em todos os objectos com um determinado conjunto de "atributos".
a apresentação mais realista de linguagens orientadas a objeto permitiria objetos que são elementos de tipos recursivamente definidas .
neste contexto, delimitada a quantificação não serve mais ao seu objetivo.É fácil encontrar funções que façam sentido em todos os objetos com um conjunto especificado de métodos, mas que não possam ser digitadas no sistema Cardelli-Wegner.
Para fornecer uma base para funções polimórficas digitadas em linguagens orientadas a objetos, introduzimos a quantificação limitada por F)

Duas faces das mesmas moedas

Existem duas formas principais de abstração nas linguagens de programação:

  • parametrização e
  • membros abstratos.

A primeira forma é típica para linguagens funcionais, enquanto a segunda forma é normalmente usada em linguagens orientadas a objetos.

Tradicionalmente, Java suporta parametrização para valores e abstração de membro para operações. O Java 5.0 mais recente com genéricos também suporta parametrização para tipos.

Os argumentos para incluir genéricos no Scala são duplos:

  • Primeiro, a codificação para tipos abstratos não é tão simples de fazer manualmente. Além da perda de concisão, também há o problema de conflitos acidentais de nomes entre nomes abstratos de tipos que emulam parâmetros de tipos.

  • Segundo, tipos genéricos e abstratos geralmente desempenham papéis distintos nos programas Scala.

    • Geralmente, os genéricos são usados ​​quando se precisa apenas digitar instanciação , enquanto
    • tipos abstratos geralmente são usados ​​quando é necessário fazer referência ao tipo abstrato do código do cliente .
      Este último surge em particular em duas situações:
    • Pode-se ocultar a definição exata de um membro do tipo do código do cliente, para obter um tipo de encapsulamento conhecido nos sistemas de módulos no estilo SML.
    • Ou pode-se substituir o tipo covariantemente nas subclasses para obter o polimorfismo da família.

Em um sistema com polimorfismo limitado, a reescrita de tipos abstratos em genéricos pode acarretar uma expansão quadrática dos limites de tipos .


Atualização outubro de 2009

Membros de tipo abstrato versus parâmetros de tipo genérico em Scala (Bill Venners)

(ênfase minha)

Minha observação até agora sobre membros de tipo abstrato é que eles são principalmente uma escolha melhor do que parâmetros de tipo genérico quando:

  • você deseja permitir que as pessoas misturem definições desses tipos por meio de características .
  • você acha que a menção explícita do nome do membro do tipo quando ele está sendo definido ajudará na legibilidade do código .

Exemplo:

se você quiser passar três objetos diferentes de fixação nos testes, poderá fazê-lo, mas precisará especificar três tipos, um para cada parâmetro. Assim, se eu tivesse adotado a abordagem de parâmetro de tipo, suas classes de suíte poderiam ter ficado assim:

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

Considerando que, com a abordagem de membro de tipo, ficará assim:

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

Uma outra diferença menor entre membros de tipo abstrato e parâmetros de tipo genérico é que, quando um parâmetro de tipo genérico é especificado, os leitores do código não veem o nome do parâmetro de tipo. Assim, alguém viu esta linha de código:

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

Eles não saberiam qual era o nome do parâmetro de tipo especificado como StringBuilder sem procurá-lo. Enquanto o nome do parâmetro type está bem ali no código na abordagem de membro do tipo abstract:

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

No último caso, os leitores do código podem ver que StringBuilderé o tipo "parâmetro de fixação".
Eles ainda precisariam descobrir o que "parâmetro de fixação" significava, mas poderiam pelo menos obter o nome do tipo sem consultar a documentação.

VonC
fonte
61
Como devo obter pontos de carma, respondendo às perguntas do Scala quando você vem fazer isso ??? :-)
Daniel C. Sobral
7
Oi Daniel: Acho que deve haver exemplos concretos para ilustrar as vantagens dos tipos abstratos em relação à parametrização. A publicação de alguns neste tópico seria um bom começo;) Eu sei que iria aprovar isso.
VonC 21/07/2009
1
Você acha que o seguinte é um resumo justo: Resumo Os tipos são usados ​​em relacionamentos 'has-a' ou 'uses-a' (por exemplo, uma vaca come capim), onde os genéricos geralmente são relacionamentos 'de' (por exemplo, lista de entradas)
thatismatt 22/07/2009
Não sei se o relacionamento é tão diferente entre o uso de tipos abstratos ou genéricos. O que é diferente é como eles são usados ​​e como os limites dos parâmetros são gerenciados. Mais na minha resposta em um momento.
VonC 22/07/2009
1
Nota para si mesmo: consulte também esta postagem no blog de maio de 2010: daily-scala.blogspot.com/2010/05/…
VonC
37

Eu tinha a mesma pergunta quando estava lendo sobre Scala.

A vantagem de usar genéricos é que você está criando uma família de tipos. Ninguém precisará subclasse Buffer-que pode apenas usar Buffer[Any], Buffer[String]etc.

Se você usar um tipo abstrato, as pessoas serão forçadas a criar uma subclasse. As pessoas terão classes como AnyBuffer, StringBuffer, etc.

Você precisa decidir qual é o melhor para sua necessidade específica.

Daniel Yankowsky
fonte
18
mmm thins melhorou muito nessa frente, você pode exigir Buffer { type T <: String }ou Buffer { type T = String }dependendo das suas necessidades
Eduardo Pareja Tobes
21

Você pode usar tipos abstratos em conjunto com parâmetros de tipo para estabelecer modelos personalizados.

Vamos supor que você precise estabelecer um padrão com três características conectadas:

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]

da maneira que os argumentos mencionados nos parâmetros de tipo são AA, BB, CC, respeitosamente

Você pode vir com algum tipo de código:

trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]

o que não funcionaria dessa maneira simples por causa das ligações de parâmetro de tipo. Você precisa torná-lo covariante para herdar corretamente

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]

Essa amostra seria compilada, mas estabelece requisitos rígidos para as regras de variação e não pode ser usada em algumas ocasiões

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
  def forth(x:B):C
  def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
  def forth(x:C):A
  def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
  def forth(x:A):B
  def back(x:B):A
}

O compilador fará objeção com vários erros de verificação de variação

Nesse caso, você pode reunir todos os requisitos de tipo em característica adicional e parametrizar outras características sobre ele

//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
  type A <: AA[O]
  type B <: BB[O]
  type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:B):C
  def right(r:C):B = r.left(this)
  def join(l:B, r:C):A
  def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:C):A
  def right(r:A):C = r.left(this)
  def join(l:C, r:A):B
  def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:A):B
  def right(r:B):A = r.left(this)
  def join(l:A, r:B):C
  def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}

Agora podemos escrever uma representação concreta para o padrão descrito, definir os métodos left e join em todas as classes e acertar e dobrar de graça

class ReprO extends OO[ReprO] {
  override type A = ReprA
  override type B = ReprB
  override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
  override def left(l:B):C = ReprC(data - l.data)
  override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
  override def left(l:C):A = ReprA(data - l.data)
  override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
  override def left(l:A):B = ReprB(data - l.data)
  override def join(l:A, r:B):C = ReprC(l.data + r.data)
}

Portanto, os tipos abstratos e os parâmetros de tipo são usados ​​para criar abstrações. Ambos têm pontos fracos e fortes. Os tipos abstratos são mais específicos e capazes de descrever qualquer estrutura de tipos, mas são detalhados e requerem a especificação explícita. Os parâmetros de tipo podem criar vários tipos instantaneamente, mas oferecem uma preocupação adicional sobre herança e limites de tipo.

Eles dão sinergia entre si e podem ser usados ​​em conjunto para criar abstrações complexas que não podem ser expressas com apenas uma delas.

ayvango
fonte
0

Eu acho que não há muita diferença aqui. Os membros abstratos do tipo podem ser vistos apenas como tipos existenciais, semelhantes aos tipos de registro em algumas outras linguagens funcionais.

Por exemplo, temos:

class ListT {
  type T
  ...
}

e

class List[T] {...}

Então ListTé exatamente o mesmo que List[_]. A conveniência dos membros do tipo é que podemos usar a classe sem um tipo concreto explícito e evitar muitos parâmetros de tipo.

Comcx
fonte