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 Animal
com 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 SuitableFood
qual eu não conheço.
Portanto, é um tipo abstrato. Você não fornece uma implementação do tipo. Então você tem um eat
método que come apenas SuitableFood
.
E então na Cow
classe eu dizia: OK, eu tenho uma vaca, que estende a classe Animal
e 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 O
de 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.
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 usarBuffer[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.
fonte
Buffer { type T <: String }
ouBuffer { type T = String }
dependendo das suas necessidadesVocê 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:
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:
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
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
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
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
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.
fonte
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:
e
Então
ListT
é exatamente o mesmo queList[_]
. 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.fonte