• Nenhum resultado encontrado

Tipos compostos e construídos

Embora os tipos básicos de uma linguagem de programação normalmente ofereçam uma abstração adequada dos tipos reais de dados tratados diretamente pelo hardware, frequentemente são inadequados para representar o domínio de informações necessário aos programas. Os programas rotineiramente lidam com estruturas de dados mais com- plexas, como grafos, árvores, tabelas, arrays, registros, listas e pilhas. Essas estruturas consistem em um ou mais objetos, cada um com seu próprio tipo. A capacidade de cons- truir novos tipos para esses objetos compostos e agregados é um recurso essencial de muitas linguagens de programação, que permite ao programador organizar informações de maneiras novas e específicas ao programa. Unir essa organização ao sistema de tipos melhora a capacidade do compilador de detectar programas malformados, e também permite que a linguagem expresse operações de nível mais alto, como uma atribuição de estrutura inteira.

Veja, por exemplo, o caso da linguagem Lisp, que oferece bastante suporte para a programação com listas. Nela, uma lista é um tipo construído. Uma lista é, ou o valor designado nil, ou (cons first rest), onde first é um objeto, rest é uma lista, e cons é um construtor que cria uma lista a partir de seus dois argumentos. Esta implementação Lisp pode verificar cada chamada a cons para garantir que seu segundo argumento seja, de fato, uma lista.

Arrays

Arrays estão entre os objetos agregados mais utilizados. Um array agrupa múltiplos objetos do mesmo tipo e oferece um nome distinto a cada um deles — apesar de ser

um nome implícito, calculado, ao invés de um explícito, designado pelo programador. A declaração C int a[100][200]; reserva espaço para 100 × 200 = 20.000 inteiros e garante que eles possam ser endereçados usando o nome a. As referências a[1][17] e a[2][30] acessam locais de memória distintos e independentes. A propriedade essencial de um array é que o programa pode computar nomes para cada um de seus elementos usando números (ou algum outro tipo ordenado, discreto) como subscritos.

O suporte para operações sobre arrays varia bastante. FORTRAN 90, PL/I e APL admitem a atribuição de arrays inteiros ou parciais, e, também, a aplicação elemento a elemento de operações aritméticas com arrays. Para os arrays 10 × 10 x, y e z, indexados de 1 a 10, a instrução x = y + z sobrescreveria cada x[i,j] como y[i,j] + z[i,j] para todo 1 ≤ i, j ≤ 10. APL usa a noção de operações com array mais do que na maioria das linguagens, e inclui operadores para produto interno, produto externo e vários tipos de reduções. Por exemplo, a redução de soma de y, es- crita como x ← +/y, atribui a x a soma escalar dos elementos de y.

Um array pode ser visto como um tipo construído, pois o construímos especificando o tipo dos seus elementos. Assim, um array 10 × 10 de inteiros tem o tipo array bidimensional de inteiros. Algumas linguagens incluem as dimensões do array em seu tipo; assim, um array 10 × 10 de inteiros tem um tipo diferente de um array 12 × 12 de inteiros. Isto permite que o compilador capture operações de array em que as dimensões são incompatíveis como um erro de tipo. A maioria das linguagens permite arrays de qualquer tipo básico; algumas linguagens também permitem arrays de tipos construídos.

Strings

Algumas linguagens de programação tratam as strings como um tipo construído. PL/I, por exemplo, tem strings de bits e de caracteres. Propriedades, atributos e operações definidas nesses dois tipos são semelhantes, e são propriedades de uma string. O intervalo de valores permitidos em qualquer posição difere entre uma string de bits e outra de caracteres. Assim, visualizá-los como uma string de bits e string de caracteres é apropriado. (A maioria das linguagens que admite strings limita o suporte embutido a um único tipo de string — a de caracteres.) Outras linguagens, como C, admitem strings de caracteres tratando-as como arrays de caracteres.

Um verdadeiro tipo string difere de um tipo array de várias maneiras importantes. As operações que fazem sentido em strings, como concatenação, tradução e cálculo do tamanho, podem não ter correspondentes para arrays. Conceitualmente, a comparação de strings deve funcionar a partir da ordem lexicográfica, de modo que “a” < “boo” e “fee” < “fie”. Os operadores de comparação-padrão podem ser sobrecarregados e usados na forma natural. A implementação da comparação para um array de caracteres sugere uma comparação equivalente para um array de números ou de estruturas, em que a analogia com strings pode não ser mantida. De modo semelhante, o tamanho real de uma string pode diferir do seu tama- nho alocado, embora a maioria dos usos de um array utilize todos os elementos alocados.

Tipos enumerados

Muitas linguagens permitem que o programador crie um tipo que contém um conjunto específico de valores constantes. O tipo enumerado, introduzido em Pascal, permite que o programador use nomes autodocumentáveis para pequenos conjuntos de cons- tantes. Exemplos clássicos incluem dias da semana e meses. Em sintaxe C, estes poderiam ser:

que cada tipo enumerado se comporte como se fosse uma subfaixa dos inteiros. Por exemplo, o programador pode declarar um array indexado pelos elementos de um tipo enumerado.

Estruturas e variantes

Estruturas, ou registros, agrupam vários objetos de um tipo qualquer. Os elementos, ou membros, da estrutura normalmente recebem nomes explícitos. Por exemplo, um programador implementando uma árvore sintática em C poderia precisar de nós com um e dois filhos.

O tipo de uma estrutura é o produto ordenado dos tipos dos elementos individuais que ela contém. Assim, poderíamos descrever o tipo de um Node1 como (Node1 *) × unsigned × int, enquanto um Node2 seria (Node2 *) × (Node2 *) × unsigned × int. Esses novos tipos deverão ter as mesmas propriedades essenciais que um tipo básico tem. Em C, o autoincremento de um ponteiro para um Node1 ou a conversão de um ponteiro para um Node1 * tem o efeito desejado — o comportamento é semelhante ao que acontece para um tipo básico.

Muitas linguagens de programação permitem a criação de um tipo que é a união de outros tipos. Por exemplo, alguma variável x pode ter o tipo integer ou boolean ou DiaSemana. Em Pascal, isto é feito com registros variantes — registro é o termo em Pascal para uma estrutura. Em C, isto é feito com uma union. O tipo de uma union é uma união dos seus tipos componentes; assim, nossa variável x tem o tipo integer ∪ boolean ∪ DiaSemana. Uniões também podem incluir estruturas de tipos distintos, mesmo quando os tipos de estruturas individuais têm tamanhos diferentes. A linguagem precisa oferecer um mecanismo para referenciar cada campo de forma não ambígua.

UMA VISÃO ALTERNATIVA DAS ESTRUTURAS

A visão clássica das estruturas trata cada tipo de estrutura como um tipo distinto. Esta técnica para os tipos de estrutura segue o tratamento de outras agregações, como arrays e strings; ela parece natural, e faz distinções que são úteis para o programador. Por exemplo, um nó de árvore com dois filhos provavelmente deve ter um tipo diferente de um nó de árvore com três filhos; presume-se que eles sejam usados em situações diferentes. Um programa que atribui um nó de três filhos a um nó de dois filhos deve gerar um erro de tipo e uma mensagem de advertência para o programador.

Sob o ponto de vista do sistema de runtime, porém, tratar cada estrutura como um tipo distinto complica o quadro. Com tipos de estrutura distintos, a heap contém um conjunto qualquer de objetos retirados de um conjunto qualquer de tipos. Isto torna difícil raciocinar sobre programas que lidam diretamente com os objetos na heap, como um coletor de lixo, por exemplo. Para simplificar tais programas, seus autores às vezes usam uma técnica diferente para os tipos de estrutura.

Esse modelo alternativo considera todas as estruturas no programa como de um único tipo. As declarações de estruturas individuais criam, cada uma, uma forma variante do tipo structure. Este tipo, por si só, é a união de todas essas variantes. Esta técnica permite que o programa veja a heap como uma coleção de objetos de um único tipo, ao invés de uma coleção de muitos tipos. Esta visão torna o código que manipula a heap muito mais simples de analisar e otimizar.

Ponteiros

Estes são endereços de memória abstratos, que permitem que o programador manipule quaisquer estruturas de dados. Muitas linguagens incluem um tipo ponteiro. Ponteiros permitem que um programa salve um endereço e mais tarde examine o objeto que ele endereça. Ponteiros são criados quando os objetos são criados (new em Java ou malloc em C). Algumas linguagens oferecem um operador que retorna o endereço de um objeto, como o operador & em C.

Para evitar que os programadores usem um ponteiro para o tipo t a fim de referenciar uma estrutura do tipo s, algumas linguagens restringem a atribuição de ponteiro para tipos “equivalentes”. Nelas, o ponteiro no lado esquerdo de uma atribuição precisa ter o mesmo tipo da expressão no lado direito. Um programa pode legalmente atribuir um ponteiro para inteiro a uma variável declarada como ponteiro para inteiro, mas não para uma declarada como ponteiro para ponteiro para inteiro ou ponteiro para booleano. Estas últimas são abstrações ilegais ou exigem uma conversão explícita pelo programador.

Naturalmente, o mecanismo para criar novos objetos deve retornar um objeto do tipo apropriado. Assim, new, de Java, cria um objeto tipado; outras linguagens usam uma rotina polimórfica que toma o tipo de retorno como um parâmetro. ANSI C trata disto de um modo incomum: a rotina de alocação padrão malloc retorna um ponteiro para void, o que força o programador a converter (cast) o valor retornado por cada chamada a malloc. Algumas linguagens permitem a manipulação direta de ponteiros. A aritmética sobre ponteiros, incluindo autoincremento e autodecremento, permite que o programa cons- trua novos ponteiros. C utiliza o tipo de um ponteiro para determinar as magnitudes de autoincremento e autodecremento. O programador pode definir um ponteiro para o início de um array; o autoincremento avança o ponteiro de um elemento no array para o próximo elemento.

O operador de endereço, quando aplicado a um objeto do tipo t, retorna um valor do tipo ponteiro para t.

Polimorfismo

Uma função que pode operar sobre argumentos de diferentes tipos é uma função polimórfica. Se o conjunto de tipos tiver que ser especificado explicitamente, a função usa o polimorfismo ad hoc; se o corpo da função não especificar tipos, usa o polimorfismo paramétrico.

Historicamente, dois métodos gerais tem sido testados. O primeiro, equivalência de nomes, afirma que dois tipos são equivalentes se e somente se ambos tiverem o mes- mo nome. Filosoficamente, esta regra considera que o programador pode selecionar qualquer nome para um tipo; se escolher nomes diferentes, a linguagem e sua im- plementação devem honrar este ato deliberado. Infelizmente, a dificuldade de manter nomes consistentes aumenta com o tamanho do programa, com o número de autores e com o número de arquivos de código distintos.

O segundo método, equivalência estrutural, declara que dois tipos são equivalentes se e somente se ambos tiverem a mesma estrutura. Filosoficamente, esta regra declara que dois objetos são intercambiáveis se consistirem no mesmo conjunto de campos, na mesma ordem, e todos esses campos tiverem tipos equivalentes. A equivalência estrutural examina as propriedades essenciais que definem o tipo.

Cada política tem pontos fortes e fracos. A equivalência de nomes considera que nomes idênticos ocorrem como um ato deliberado; em um grande projeto de programação, isto requer disciplina para evitar conflitos não intencionais. A equivalência estrutural assume que objetos intercambiáveis podem ser usados com segurança um no lugar do outro; mas pode criar problemas se alguns dos valores tiverem significados “especiais”. (Imagine dois tipos hipotéticos, estruturalmente idênticos. O primeiro contém um bloco de controle do sistema de E/S, enquanto o segundo, contém a coleção de informações sobre uma imagem na tela como um mapa de bits. Tratá-los como tipos distintos permitiria que o compilador detectasse um uso indevido — passando o bloco de controle de E/S para uma rotina de atualização de tela —, mas isto não aconteceria ao tratá-los como o mesmo tipo.)

REPRESENTAÇÃO DE TIPOS

Assim como a maioria dos objetos que um compilador precisa manipular, os tipos precisam de uma representação interna. Algumas linguagens, como FORTRAN 77, possuem um pequeno conjunto fixo de tipos. Para estas linguagens, uma pequena tag de inteiros é tão eficiente quanto suficiente. Porém, muitas linguagens modernas possuem sistemas de tipos abertos. Para estas, o construtor de compiladores precisa projetar uma estrutura que possa representar tipos arbitrários.

Se o sistema de tipos for baseado em equivalência de nomes, qualquer quantidade de representações simples será suficiente, desde que o compilador possa usar a representação para rastrear até uma representação da estrutura real. Se o sistema de tipos for baseado em equivalência estrutural, a representação do tipo precisa codificar sua estrutura. A maioria desses sistemas constrói árvores para representar tipos; constroem uma árvore para cada declaração de tipo e comparam estruturas de árvore para testar a equivalência.