Por exemplo, em OCaml o tipo int representa o conjunto do inteiros, tem associados os literais ..., -2, -1, 0, 1, 2, etc., e as operações +, -, *, div, mod, succ, etc.
Numa linguagem sem tipos, e.g. Assembler, uma operação pode ser aplicada a quaisquer dados sem que qualquer validação seja feita. Os dados são vistos como simples sequências de bits e cada operação interpreta cada sequência de bits da maneira que lhe convém.
Numa linguagem com tipos, cada elemento de dados tem um tipo associado. Ao tentar aplicar uma operação a um valor que não tenha o tipo esperado, obtém-se um erro de tipo.
Um sistema de tipos contribui para dar significado aos programas: ao associar um tipo a uma sequência de bits, estamos a atribuir um significado a essa sequência de bits.
Para que servem os tipos?
No caso das linguagens com tipificação estática:
No caso das linguagens com tipificação dinâmica:
Para exemplificar, eis três regras que fazem parte do sistema de tipos da linguagem OCaml:
exp : bool exp' : bool -------------- ------------- --------------------------- false : bool true : bool exp && exp' : bool
Não mostramos mais regras, pois a formalização de sistemas de tipo não é matéria de LAP. Mas, pelo menos, fica a ideia.
Estas regras são úteis por várias razões. Por exemplo: servem para o programador compreender melhor a linguagem; servem para provar que o sistema de tipos é consistente (ou seja, que a cada expressão é associado um tipo único); servem como base para a escrita duma parte importante do compilador.
Exemplo na linguagem Scala:
def takeoff(runway: Int, p: { val callsign: String; def fly(height: Int) }) = { tower.print(p.callsign + " requests take-off on runway " + runway) tower.read(p.callsign + " is clear for take-off") p.fly(1000) }
Exemplo na linguagem Java:
def takeoff(Int runway, Plane p) = { tower.print(p.callsign + " requests take-off on runway " + runway) tower.read(p.callsign + " is clear for take-off") p.fly(1000) }Nas linguagens orientadas pelos objetos que usam tipificação dinâmica, o sistema de tipos tem a seguinte designação:
Exemplo na linguagem JavaScript:
def takeoff(runway, p) = { tower.print(p.callsign + " requests take-off on runway " + runway) tower.read(p.callsign + " is clear for take-off") p.fly(1000) }
As seguintes linguagens usam tipificação estática: OCaml, C, C++ e Java.
Numa linguagem com tipificação estática, repare que os tipos são associados às expressões que aparecem no texto dos programas e é o próprio texto que é validado. Depois, durante a execução do programa já compilado, os tipos dos valores são ignorados porque o compilador já garantiu a ausência de erros de tipo.
Mas atenção: Num programa validado, ou seja num programa sem erros de tipo, podem mesmo assim ocorrer outro género de problemas durante a execução:
Por exemplo, no código Java que se segue, a variável a, de tipo Animal recebe um gato. Depois, mais adiante, tenta-se fazer miar o animal referido pela variável a. Mas o compilador tem considerar que, nessa altura, a variável a poderá já não referir um gato e, por isso, produz um erro de compilação; no entanto, em tempo de execução até podia não haver problema se estivesse um gato na variável...
Animal a = new Cat(); ... a.meou(); // ERRO DE TIPO
As seguintes linguagens usam tipificação dinâmica: JavaScript, Prolog, Lisp, Perl, Python, Ruby, APL e Smalltalk.
Numa linguagem com tipificação dinâmica, os tipos são associados aos valores que são usados em tempo de execução e não às variáveis. Portanto, em tempo de execução tem de existir informação de tipo associada aos valores. Sempre que se aplica uma operação a alguns valores, o tipo desses valores é testado pelo sistema de execução.
A limitação característica dos sistemas de tipos dinâmicos é o facto dos erros de tipo só serem detetados durante a execução dos programas. Mesmo um erro de tipo básico pode ficar por descobrir durante muito tempo, por se se situar em código que é executado muito raramente. O erro só será apanhado quando esse código for executado pela primeira vez.
Repare também que numa linguagem com tipificação dinâmica se gasta mais memória com a informação de tipo que é guardada nos valores e se gasta mais tempo a fazer validação de tipos em tempo de execução.
Por exemplo, no código JavaScript que se segue, a variável a recebe um gato. Depois, mais adiante, tenta-se fazer miar o animal referido por essa variável a. O compilador aceita o código, mas depois em tempo de execução, poderá ser detetado ou não um erro de tipo. Se a variável a referir mesmo um gato, tudo correrá bem. Se referir um valor de outro tipo, então ocorrerá um erro de tipo e será lançada uma exceção.
let a = new Cat(); ... a.meou();
Um exemplo em Java: a operação instanceof permite ao programador testar dinamicamente o tipo de qualquer objeto; a operação de cast aplicada a um objeto também executa um teste de tipo implícito em tempo de execução. Para isto funcionar, os objetos em Java precisam de registar a classe a que pertencem.
Por exemplo, os adeptos da tipificação estática acreditam que os seus programas são mais seguros depois de verificados, mas os adeptos da tipificação dinâmica argumentam que conseguem programar melhor as suas ideias sem constrangimentos artificiais e que apesar de tudo os seus programas têm provado ser robustos e conter poucos erros.
Na verdade é perfeitamente possível ser bons resultados dentro de cada uma das escolas de programação desde que se usem boas técnicas de desenvolvimento de software. Uma das técnica mais importante é certamente a técnica de escrever muitos testes unitários para validar sistematicamente todos os aspetos do software em desenvolvimento.
Só uma curiosidade: O CLIP, o sistema da informação da FCT, é um exemplo de software da escola da tipificação dinâmica - está escrito em Prolog.
Os sistemas de tipos do C e C++ têm falhas porque:
Exemplos de linguagens com sistemas de tipos sem falhas:
Repare que na lista de linguagens com sistemas de tipos sem falhas aparecem linguagens com tipificação estática e linguagens com tipificação dinâmica.
Uma função polimórfica é uma função que pode ser aplicada a argumentos de vários tipos. A nossa conhecida função len em OCaml é polimórfica pois aplica-se a listas de qualquer tipo:
len : 'a list -> int
Um tipo polimórfico é um tipo cujas operações se aplicam a valores de mais do que um tipo. Em OCaml o tipo da listas 'a list é polimórfico. Em Java o tipo Vector<E> também é.
Uma variável polimórfica é uma variável que pode conter valores de tipos diferentes. Em Java, uma variável de tipo Animal pode referir qualquer objeto cujo tipo seja subtipo de Animal. Em C uma variável de tipo void * pode guardar qualquer apontador.
Entidades que não sejam polimórficas dizem-se monomórficas.
Muitas das linguagens com tipificação estática modernas suportam polimorfismo. Todas as linguagens com tipificação dinâmica suportam polimorfismo de forma inerente.
Polimorfismo universal - A função trabalha de forma uniforme sobre uma diversidade infinita de tipos que partilham a mesma estrutura. A implementação é única e o mesmo código consegue lidar com todos os tipos considerados.
Polimorfismo paramétrico - É uma forma de polimorfismo universal onde a função polimórfica tem um parâmetro de tipo implícito ou explicito. Na chamada da função o parâmetro de tipo pode ser ou não inferido. A função len em OCaml é polimórfica paramétrica, sendo 'a o nome do parâmetro de tipo:
len : 'a list -> int let rec len l = match l with [] -> 0 | x::xs -> 1 + len xs ;;A seguinte função em Java é polimórfica paramétrica, sendo T o nome do parâmetro de tipo:
<T> void fromArrayToCollection(T[] a, Collection<T> c) { for (T o : a) { c.add(o); }}
Polimorfismo de inclusão - É uma forma de polimorfismo universal que resulta da noção de subtipo. Uma função que declare aceita argumentos dum dado tipo, digamos Animal, também aceita argumentos de subtipos desse tipo, digamos Cat ou Lion. Qualquer linguagens com subtipos suporta polimorfismo de inclusão.
A seguinte função em Java é polimórfica de inclusão:
int weight(Animal a) { ... }
Polimorfismo ad hoc - A função trabalha de forma não uniforme sobre uma diversidade finita de tipos que não partilham a mesma estrutura. Existem múltiplas implementações, uma para cada tipo considerado.
Overloading - O mesmo nome de função é usado para denotar diferentes implementações monomórficas. No ponto da chamada usa-se o contexto para descobrir qual das implementações deve ser usada. Portanto esta forma de polimorfismo não é mais do que uma conveniência sintática. Um exemplo: o operador "+" em Java denota três operações monomórficas distintas, não relacionadas entre si: (1) soma de inteiros; (2) soma de reais; (3) concatenação de strings.
Coerção - Uma coerção é uma conversão automática de tipo. As coerções fazem com que funções essencialmente monomórficas se tornem polimórficas, pois passam a poder ser chamadas com argumentos de diferentes tipos. A seguinte função em C foi escrita para ser monomórfica
double inc(double d) { return d + 1; }mas devido ao facto de em C existir coerção de inteiros para doubles, a função passa a poder ser aplicada tanto a reais como inteiros.
Lendo as descrição anteriores, percebe-se porque razão o "polimorfismo ad hoc" também se chama "polimorfismo aparente". É apenas "açúcar sintático" que está em jogo.