Apagamento de tipo é bom
Vamos nos ater aos fatos
Muitas das respostas até agora estão excessivamente preocupadas com o usuário do Twitter. É útil manter o foco nas mensagens e não no mensageiro. Há uma mensagem bastante consistente mesmo apenas com os trechos mencionados até agora:
É engraçado quando os usuários Java reclamam do apagamento de tipo, que é a única coisa que o Java acertou, enquanto ignoram todas as coisas erradas.
Eu obtenho enormes benefícios (por exemplo, parametricidade) e custo nulo (o custo alegado é um limite da imaginação).
new T é um programa quebrado. É isomórfico à afirmação "todas as proposições são verdadeiras". Eu não sou grande nisso.
Uma meta: programas razoáveis
Esses tweets refletem uma perspectiva que não está interessada em saber se podemos fazer a máquina fazer algo , mas mais se podemos raciocinar que a máquina fará algo que realmente queremos. O bom raciocínio é uma prova. As provas podem ser especificadas em notação formal ou algo menos formal. Independentemente da linguagem de especificação, eles devem ser claros e rigorosos. As especificações informais não são impossíveis de estruturar corretamente, mas costumam apresentar falhas na programação prática. Acabamos com remediações como testes automatizados e exploratórios para compensar os problemas que temos com o raciocínio informal. Isso não quer dizer que o teste seja intrinsecamente uma má ideia, mas o usuário do Twitter citado está sugerindo que existe uma maneira muito melhor.
Portanto, nosso objetivo é ter programas corretos sobre os quais possamos raciocinar de forma clara e rigorosa de uma forma que corresponda a como a máquina realmente executará o programa. Este, porém, não é o único objetivo. Também queremos que nossa lógica tenha um certo grau de expressividade. Por exemplo, há muito que podemos expressar com a lógica proposicional. É bom ter quantificação universal (∀) e existencial (∃) de algo como lógica de primeira ordem.
Usando sistemas de tipo para raciocinar
Esses objetivos podem ser muito bem tratados por sistemas de tipos. Isso é especialmente claro por causa da correspondência Curry-Howard . Essa correspondência é freqüentemente expressa com a seguinte analogia: os tipos estão para os programas assim como os teoremas estão para as provas.
Essa correspondência é um tanto profunda. Podemos pegar expressões lógicas e traduzi-las por meio da correspondência de tipos. Então, se temos um programa com a mesma assinatura de tipo que compila, provamos que a expressão lógica é universalmente verdadeira (uma tautologia). Isso ocorre porque a correspondência é bidirecional. A transformação entre o tipo / programa e os mundos do teorema / prova é mecânica e pode, em muitos casos, ser automatizada.
Curry-Howard joga bem com o que gostaríamos de fazer com as especificações de um programa.
Os sistemas de tipos são úteis em Java?
Mesmo com uma compreensão de Curry-Howard, algumas pessoas acham fácil descartar o valor de um sistema de tipos, quando
- é extremamente difícil de trabalhar
- corresponde (por meio de Curry-Howard) a uma lógica com expressividade limitada
- está quebrado (o que leva à caracterização dos sistemas como "fracos" ou "fortes").
Com relação ao primeiro ponto, talvez os IDEs tornem o sistema de tipos Java fácil de trabalhar (isso é altamente subjetivo).
Em relação ao segundo ponto, Java passa a corresponder quase a uma lógica de primeira ordem. Os genéricos fornecem o equivalente ao sistema de tipos da quantificação universal. Infelizmente, os curingas nos fornecem apenas uma pequena fração da quantificação existencial. Mas a quantificação universal é um bom começo. É bom poder dizer que funciona List<A>
universalmente para todas as listas possíveis porque A é completamente irrestrito. Isso leva ao que o usuário do Twitter está falando com respeito à "parametricidade".
Um artigo frequentemente citado sobre parametricidade são os Teoremas de Philip Wadler gratuitamente! . O que é interessante sobre este artigo é que apenas com a assinatura de tipo sozinha, podemos provar alguns invariantes muito interessantes. Se tivéssemos que escrever testes automatizados para essas invariantes, estaríamos perdendo muito nosso tempo. Por exemplo, para List<A>
, da assinatura de tipo sozinha paraflatten
<A> List<A> flatten(List<List<A>> nestedLists);
podemos raciocinar que
flatten(nestedList.map(l -> l.map(any_function)))
≡ flatten(nestList).map(any_function)
Este é um exemplo simples, e você provavelmente pode raciocinar sobre isso informalmente, mas é ainda mais agradável quando obtemos essas provas formalmente de graça no sistema de tipos e verificadas pelo compilador.
Não apagar pode levar a abusos
Do ponto de vista da implementação da linguagem, os genéricos de Java (que correspondem aos tipos universais) jogam fortemente na parametricidade usada para obter provas sobre o que nossos programas fazem. Isso leva ao terceiro problema mencionado. Todos esses ganhos de prova e correção requerem um sistema de tipo de som implementado sem defeitos. Java definitivamente tem alguns recursos de linguagem que nos permitem quebrar nosso raciocínio. Estes incluem, mas não estão limitados a:
- efeitos colaterais com um sistema externo
- reflexão
Os genéricos não apagados estão, de muitas maneiras, relacionados à reflexão. Sem exclusão, há informações de tempo de execução que são transportadas com a implementação e que podemos usar para projetar nossos algoritmos. O que isso significa é que, estaticamente, quando raciocinamos sobre programas, não temos o quadro completo. A reflexão ameaça severamente a correção de quaisquer provas sobre as quais raciocinamos estaticamente. Não é por acaso que a reflexão também leva a uma variedade de defeitos complicados.
Então, de que maneiras os genéricos não apagados podem ser "úteis"? Vamos considerar o uso mencionado no tweet:
<T> T broken { return new T(); }
O que acontece se T não tiver um construtor sem arg? Em alguns idiomas, o que você obtém é nulo. Ou talvez você ignore o valor nulo e vá direto para o lançamento de uma exceção (à qual os valores nulos parecem levar de qualquer maneira). Como nossa linguagem é Turing completa, é impossível raciocinar sobre quais chamadas para broken
envolverão tipos "seguros" com construtores sem arg e quais não. Perdemos a certeza de que nosso programa funciona universalmente.
Apagar significa que raciocinamos (então vamos apagar)
Portanto, se quisermos raciocinar sobre nossos programas, somos fortemente aconselhados a não empregar recursos de linguagem que ameacem fortemente nosso raciocínio. Depois de fazer isso, por que não simplesmente descartar os tipos em tempo de execução? Eles não são necessários. Podemos obter alguma eficiência e simplicidade com a satisfação de que nenhuma conversão falhará ou que métodos podem estar faltando na invocação.
Apagar incentiva o raciocínio.