om – uma linguagem de programação multiparadigma

Transcrição

om – uma linguagem de programação multiparadigma
ARTUR MIGUEL DE ANDRADE VIEIRA DIAS
OM – UMA LINGUAGEM DE
PROGRAMAÇÃO MULTIPARADIGMA
Dissertação apresentada para obtenção
do Grau de Doutor em Informática
pela Universidade Nova de Lisboa,
Faculdade de Ciências e Tecnologia.
Lisboa
1999
Autor:
Artur Miguel de Andrade Vieira Dias
Título:
OM – Uma Linguagem de Programação Multiparadigma
Orientador:
António Beça Gonçalves Porto
Instituição:
Universidade Nova de Lisboa
Faculdade de Ciências e Tecnologia
Departamento de Informática
Endereço:
Quinta da Torre
2825-114 Monte da Caparica
Portugal
Copyright:
Local:
Data:
Universidade Nova de Lisboa
Lisboa
1999
Agradecimentos
É com muito gosto que agradeço a todas as pessoas e instituições que me ajudaram a concretizar a presente dissertação:
A António Porto, meu orientador, agradeço a possibilidade que me deu de enveredar pela
linha de investigação desta tese, pela sua ajuda, disponibilidade e liberdade de investigação
proporcionada.
A Luís Caires, agradeço o seu interesse pelo meu trabalho e as diversas discussões que
manteve comigo.
A Margarida Mamede, agradeço o incentivo e o excelente ambiente proporcionado no nosso gabinete de trabalho. Agradeço também a sua ajuda na revisão da versão final desta tese.
A Luís Monteiro, Presidente do Departamento de Informática, agradeço todo o apoio que
me dispensou, incluindo a flexibilização da minha carga horária ligada ao serviço decente.
A todos os meus colegas do Departamento de Informática, agradeço os incentivos recebidos.
À minha familia, agradeço o apoio e incentivo a este trabalho.
Ao Ministério da Educação, agradeço a bolsa que me concedeu, ao abrigo do programa
PRODEP II do Fundo Social Europeu.
Sumário
Esta tese explora a ideia da criação duma linguagem de programação multiparadigma estaticamente tipificada, na qual se pretende que os paradigmas participantes sejam integrados, não de
forma ad-hoc, mas sim de forma uniforme, com base num mecanismo primitivo de extensão
semântica. OM é o nome da linguagem concreta que se desenvolve, e modo, ou mecanismo
dos modos, é o nome do mecanismo primitivo de extensão semântica que se propõe.
O núcleo da linguagem OM é orientado pelos objectos e suporta os seguintes elementos
primitivos principais: objectos mutáveis com componentes privadas, classes, herança, subtipos, métodos binários, polimorfismo paramétrico, componentes de classe e mecanismo dos
modos. Ao longo da tese, este núcleo linguístico é construído de forma gradual, em paralelo
com o desenvolvimento do seu modelo tipificado: primeiro introduz-se um cálculo-lambda polimórfico de ordem superior, designado por F+, e depois, sobre este, ergue-se, a pouco e pouco,
o edifício da linguagem e do modelo. A noção de modo surge no culminar deste processo,
resultando natural a sua descrição no modelo. No âmbito deste modelo, sugere-se ainda uma
solução para a conhecido problema da tensão entre o mecanismo de herança e a relação de
subtipo, um problema recorrente nas linguagens orientadas pelos objectos estaticamente tipificadas com mecanismos de herança flexíveis.
O mecanismo dos modos é uma ferramenta de programação de nível meta que permite impor propriedades arbitrárias às entidades tipificadas dos programas (i.e. variáveis, expressões,
parâmetros e resultados), dependendo do modo com que essas entidades estão declaradas. Por
exemplo, as propriedades específicas duma variável inteira com modo lógico (i.e. uma
variável declarada com tipo log Int) são diferentes das propriedades específicas duma variável
inteira com modo constante (i.e. uma variável declarada com tipo const Int). Num modo consideram-se duas facetas: uma dinâmica e outra estática. A faceta dinâmica refere-se ao comportamento dinâmico dos objectos de funcionalidade modificada que o modo implementa. A faceta estática refere-se às propriedades estáticas das entidades tipificadas, propriedades que são
descritas usando regras de conversão implícita de tipo (regras de coerção) e regras de reinterpretação da sintaxe. Para tratar estes dois tipos de regras, o mecanismo dos modos incorpora
um sistema de coerções extensível, assim como um esquema simples de sobreposição de sintaxe focado nas operações primitivas da linguagem (i.e. atribuição, aplicação, envio de mensagem, etc.). A biblioteca padrão da linguagem OM disponibiliza cinco modos, já definidos, –
const, value, lazy, log, gen – e não é um sistema fechado.
O sistema de coerções extensível não é trivial e a sua versão mais intuitiva é indecidível.
Este problema de indecidibilidade só se resolveu com a descoberta duma solução pragmática,
conciliável com a utilização pretendida para o sistema na linguagem.
Abstract
This thesis explores the idea of creating a statically typed multiparadigm programming language where the participant paradigms are integrated, not in an ad-hoc fashion, but in an uniform
manner, using a primitive semantic extension mechanism. OM is the name of the concrete language that is developed, and mode, or mode mechanism, is the name of the primitive semantic
extension mechanism that is proposed.
The core of language OM is object-oriented and includes the following main primitive elements: mutable objects with hidden components, classes, inheritance, subtypes, binary methods, parametric polymorphism, class components and mode mechanism. Throughout the thesis, this linguistic core is built gradually, in parallel with the development of its typed model:
first, a high-order polymorphic lambda-calculus, called F+, is introduced, and then, on top of
it, the edifice of the language and the model raised, little by little. The concept of mode emerges in the culmination of this process, and its description in the model results as natural. Another matter that this model deals with is the well known problem of the tension between the
inheritance mechanism and the subtyping relation, a recurrent problem in statically typed object-oriented languages with a flexible inheritance mechanism.
The mode mechanism is a meta level programming tool that allows one to impose arbitrary
properties on the typed entities of the programs (i.e. variables, expressions, parameters and
results), depending on the mode used in the declaration of these entities. For example, the particular properties of an integer variable with logic mode (i.e. a variable declared with type log
Int) are different from the particular properties of an integer variable with constant mode (i.e. a
variable declared with type const Int). There are two facets in a mode: one dynamic and another
static. The dynamic facet is concerned with the dynamic behaviour of the objects of modified
functionality that the mode implements. The static facet is concerned with the static properties
of the typed entities, properties that are described using rules of implicit conversion of type
(coercion rules) and rules of reinterpretation of syntax. In order to be able to deal with these
two types of rules, the mode mechanism includes a extensible coercion system as well as a
simple scheme of syntax overloading focused on the primitive operations of the language (i.e.
attribution, application, sending of message, etc.) The standard library of the language OM includes five modes, already defined, – const, value, lazy, log, gen – and it is not a closed system.
The extensible coercion system is not trivial, and its most intuitive version is undecidable.
This problem of undecidability could only be solved with the discovery of a pragmatic solution, compatible with the intended use of the system in the language.
Résumé
Cette thèse explore l'idée de créer un langage de programmation multiparadigme statiquement
typé, où les paradigmes participants sont intégrés, pas d'une façon ad-hoc, mais d'une façon
uniforme, en utilisant un mécanisme primitif d'extension sémantique. OM est le nom du langage qu'on y développé, et mode, ou mécanisme des modes, le nom du mécanisme primitif
d'extension sémantique qu'on propose.
Le noyau du langage OM est orientée-objet et supporte les éléments primitifs principaux
suivants: objets mutables avec des composants privés, classes, héritage, sous-types, méthodes
binaires, polymorphisme paramétrique, composants de classe et mécanisme des modes. Ce
noyau linguistique est établi graduellement, parallèlement au développement de son modèle
typé: d'abord un calcul lambda polymorphe d'ordre supérieur, appelé F+, est présenté, et puis,
sur celui-ci, on érige l'édifice du langage et du modèle, peu à peu. Le concept du mode émerge
dans le point culminant de cette procédure et sa description dans le modèle résulte comme
naturelle. Une autre matière traitée par le modèle est le problème de la tension entre le mécanisme d'heritáge et la relation de sous-typage, un problème récurrent dans des langages orientée-objet statiquement typés avec des mécanismes d'héritage flexibles.
Le mécanisme des modes est un outil de programmation de niveau méta qui permet d'imposer les propriétés arbitraires aux entités typées des programmes (c.-à-d. variables, expressions,
paramètres et résultats), selon le mode utilisé dans la déclaration de ces entités. Par exemple,
les propriétés spécifiques d'une variable entière avec le mode logique (c.-à-d. une variable
déclarée avec type log Int ) sont différentes des propriétés spécifiques d'une variable entière
avec le mode constant (c.-à-d. une variable déclarée avec type const Int ). Dans un mode on
considere deux facettes: une dynamique et une statique. La facette dynamique concerne le
comportement dynamique des objets avec fonctionnalité modifiée qui le mode implémente. La
facette statique concerne les propriétés statiques des entités typées, propriétés qui sont spécifiées par des règles de conversion implicite de type (règles de coercition) et des règles de réinterprétation de syntaxe. Afin de traiter ces deux types de règles, le mécanisme des modes
inclut un système de coercition extensible et un système simple de superposition de syntaxe
focaliée sur les opérations primitives du langage (c.-à-d. attribution, application, envoi du message, etc.) La bibliothèque standard du langage OM contient cinq modes, déjà définis, - const,
value, lazy, log, gen - e ce n'est pas un système fermé.
Le système de coercition extensible n'est pas trivial, et sa version plus intuitive est indécidable. Ce problème a pu être resolu seulement avec la découverte d'une solution pragmatique,
compatible avec l'utilisation voulue pour le système dans le langage.
Índice
Agradecimentos....................................................................................................................... iii
Sumário......................................................................................................................................v
Abstract .................................................................................................................................. vii
Résumé ......................................................................................................................................ix
Índice.........................................................................................................................................xi
Capítulo 1 – Introdução ...........................................................................................................1
1.1 Paradigmas de programação .....................................................................................1
1.2 Linguagens extensíveis .............................................................................................2
1.3 Apresentação da linguagem e do trabalho ................................................................4
1.4 Estrutura da dissertação ............................................................................................5
1.5 Principais contribuições desta tese............................................................................7
Capítulo 2 – O Sistema F+ ........................................................................................................9
2.1 O sistema F e suas extensões ..................................................................................10
2.1.1 O sistema F ...............................................................................................10
2.1.2 O sistema Fω .............................................................................................11
2.1.3 Os sistemas F≤ e Fω
≤ ..................................................................................12
2.1.4 Polimorfismo paramétrico F-restringido ..................................................13
2.2 Sintaxe de F+ ...........................................................................................................14
2.2.1 Variáveis ligadas ......................................................................................16
2.3 Sistema de tipos de F+ .............................................................................................16
2.3.1 Juízos ........................................................................................................17
2.3.1.1 Contextos ...................................................................................17
2.3.1.2 Asserções ...................................................................................17
2.3.2 Sublinguagem dos tipos............................................................................18
2.3.2.1 Apresentação das regras de boa formação dos tipos .................18
2.3.2.2 Regras de boa formação dos tipos .............................................18
2.3.3 Identidade entre tipos ...............................................................................19
xii
Índice
2.3.3.1 Apresentação das regras de identidade entre tipos....................19
2.3.3.2 Regras de identidade entre tipos ...............................................20
2.3.4 Subtipos....................................................................................................21
2.3.4.1 Apresentação das regras de subtipo ..........................................21
2.3.4.2 Regras de subtipo ......................................................................23
2.3.4.3 Noção de polaridade..................................................................23
2.3.4.4 Indecidibilidade da relação de subtipo em F≤ ...........................24
2.3.4.5 F+d subsistema decidível de F+ ...................................................25
2.3.4.6 Usos distintos de F+ e de F+d ......................................................27
2.3.5 Termos .....................................................................................................27
2.3.5.1 Apresentação das regras de boa formação dos termos..............27
2.3.5.2 Regras de boa formação dos termos .........................................28
2.4 Teoria equacional para F + .......................................................................................28
2.4.1 Apresentação das regras de identidade entre termos ...............................28
2.4.2 Regras de identidade entre termos ...........................................................29
2.5 Formas derivadas ....................................................................................................30
2.5.1 Pares ordenados........................................................................................30
2.5.2 Operador de ponto fixo e valores recursivos ...........................................31
2.5.3 Declarações locais de tipos e valores .......................................................31
2.5.4 Tipos existenciais .....................................................................................32
2.5.4.1 Exemplo de tipo existencial ......................................................32
2.5.4.2 Tipos existenciais restringidos ..................................................33
2.5.4.3 Tipos existenciais F-restringidos ..............................................33
2.5.5 Concatenação de registos .........................................................................34
2.5.6 Referências ...............................................................................................36
+
2.5.6.1 Sintaxe de F&
............................................................................37
+
2.5.6.2 Regras de boa formação de F&
..................................................37
+
2.5.6.3 Semântica de F& ........................................................................37
2.5.7 Constante polimórfica nil .........................................................................40
2.6 Modelo semântico...................................................................................................41
Capítulo 3 – Linguagem sem objectos ..................................................................................43
3.1 A linguagem L3 ......................................................................................................44
3.1.1 Sintaxe......................................................................................................44
3.1.2 Relações binárias......................................................................................45
3.1.3 Programa ..................................................................................................45
Capítulo 4 – Objectos simples com herança ........................................................................47
4.1 Conceitos e mecanismos de L4...............................................................................48
4.1.1 Objectos ...................................................................................................48
Índice
xiii
4.1.2 Tipos-objecto ............................................................................................49
4.1.3 Classes ......................................................................................................49
4.1.4 Interfaces ..................................................................................................51
4.1.5 Herança, subclasses e superclasses ..........................................................51
4.1.6 “Reutilização sem reverificação” .............................................................52
4.1.7 Subtipos ....................................................................................................53
4.2 Semântica de L4 ......................................................................................................53
4.2.1 Semântica dos tipos ..................................................................................54
4.2.2 Semântica dos termos ...............................................................................54
4.2.2.1 Semântica das classes ................................................................55
4.2.2.2 Boa formação das subclasses ....................................................55
4.2.2.3 Semântica dos outros termos .....................................................56
4.3 Discussão sobre L4 .................................................................................................56
4.3.1 Inicialização dos objectos e criação de cópias modificadas .....................57
4.3.2 Problema da perda de informação ............................................................57
4.3.3 Inflexibilidade na herança do tipo de self ................................................58
4.3.4 Métodos binários ......................................................................................59
4.3.5 Tipos dinâmicos em L4 ............................................................................60
4.3.5.1 Introdução dos tipos dinâmicos .................................................60
4.3.5.2 Operação de teste de tipo...........................................................61
4.3.5.3 Operação de despromoção de tipo.............................................62
4.3.5.4 Discussão ...................................................................................62
4.3.5.5 Utilidade dos tipos dinâmicos ...................................................63
4.3.6 Simulação em L4 do sistema de tipos do C++ .........................................63
4.4 Conclusões ..............................................................................................................64
Capítulo 5 – Tipo SAMET, relações de compatibilidade e de extensão ...........................65
5.1 Conceitos e mecanismos de L5 ...............................................................................66
5.1.1 O tipo SAMET .........................................................................................66
5.1.2 Relação de extensão entre interfaces ........................................................68
5.1.3 Tipificação aberta e tipificação fixa .........................................................69
5.2 Semântica de L5 ......................................................................................................70
5.2.1 Semântica dos tipos ..................................................................................71
5.2.2 Semântica dos termos ...............................................................................73
5.2.2.1 Semântica das classes ................................................................73
5.2.2.2 Boa formação das subclasses ....................................................73
5.2.2.3 Semântica dos outros termos .....................................................75
5.2.3 Relação de subtipo vs. relação de compatibilidade ..................................75
5.3 Propriedades das relações de compatibilidade e extensão ......................................76
5.4 O operador “+” ........................................................................................................89
xiv
Índice
5.4.1 Problemas que o operador “+”resolve .....................................................90
5.4.2 Ilustração duma aplicação de “+” ............................................................90
5.4.3 Eficácia da classe +c na prática................................................................91
5.4.3.1 Métodos binários transformados não redefinidos .....................92
5.4.3.2 Métodos binários transformados redefinidos ............................92
5.4.3.3 Conclusão ..................................................................................92
5.5 Discussão sobre L5 .................................................................................................93
5.5.1 Complicação da relação de extensão .......................................................93
5.5.2 Programação genérica ..............................................................................94
5.5.2.1 Tipo heterogéneo.......................................................................95
5.5.2.2 Colecções heterogéneas ............................................................97
5.6 Conclusões ..............................................................................................................98
Capítulo 6 – Polimorfismo paramétrico .............................................................................101
6.1 Conceitos e mecanismos de L6.............................................................................102
6.1.1 Classes paramétricas ..............................................................................102
6.1.2 Funções paramétricas .............................................................................103
6.1.3 Parâmetros covariantes ..........................................................................104
6.2 Semântica de L6 ...................................................................................................106
6.2.1 Semântica dos tipos e termos .................................................................106
6.2.2 Boa tipificação da instanciação com variáveis de tipo ..........................106
6.3 Discussão sobre L6 ...............................................................................................107
6.3.1 Operações dependentes do tipo-parâmetro ............................................107
6.3.2 Polimorfismo paramétrico e coerções ....................................................108
6.3.2.1 Problema e solução .................................................................108
6.3.2.2 Exemplos.................................................................................109
6.4 Conclusões ............................................................................................................111
Capítulo 7 – Componentes privadas e variáveis de instância ..........................................113
7.1 Conceitos e mecanismos de L7.............................................................................115
7.1.1 Formas de encapsulamento ....................................................................116
7.1.2 Nomeação das componentes das classes................................................117
7.1.3 Tipo externo e tipo interno .....................................................................118
7.1.4 Interfaces global, externa, interna e secreta ...........................................119
7.1.5 SELFT, SAMET e herança ....................................................................120
7.2 Semântica de L7 ...................................................................................................120
7.2.1 Semântica dos tipos................................................................................120
7.2.2 Semântica dos termos.............................................................................123
7.2.2.1 Semântica das classes..............................................................123
7.2.2.2 Boa formação das subclasses ..................................................123
Índice
xv
7.2.2.3 Semântica dos outros termos ...................................................126
7.2.2.4 Função de ocultação ................................................................127
7.3 A linguagem imperativa L7& ................................................................................128
7.3.1 Variáveis de instância.............................................................................128
7.3.2 Semântica de L7& ..............................................................................................129
7.3.2.1 Tratamento dos pontos fixos ...............................................................129
7.3.2.2 Criação das variáveis de instância .......................................................130
7.3.2.3 Inicialização das variáveis de instância ...............................................131
7.3.2.4 Constante nil ........................................................................................132
7.3.3 Tipos-referência e herança .................................................................................133
7.4 Conclusões ............................................................................................................134
Capítulo 8 – Componentes de classe ...................................................................................137
8.1 Conceitos e mecanismos de L8 .............................................................................139
8.1.1 Componentes de classe e meta-objectos ................................................140
8.1.2 Utilidade das componentes de classe .....................................................140
8.1.3 Nomeação das componentes das classes ................................................141
8.1.4 Tipos-objecto e tipos-meta-objecto ........................................................142
8.1.5 Interfaces de classe .................................................................................143
8.1.6 Recursividade das classes e SELFC .......................................................143
8.1.7 Componentes de classe e herança ..........................................................144
8.1.8 Resumo dos nomes especiais .................................................................144
8.2 Semântica de L8 ....................................................................................................145
8.2.1 Semântica dos tipos ................................................................................146
8.2.2 Semântica dos termos .............................................................................147
8.2.2.1 Semântica das classes ..............................................................147
8.2.2.2 Boa formação das subclasses ..................................................147
8.2.2.3 Semântica dos outros termos ...................................................148
8.3 Discussão sobre L8 ...............................................................................................148
8.3.1 Polimorfismo de classe...........................................................................149
8.3.1.1 Definição de polimorfismo de classe ......................................149
8.3.1.2 Boa tipificação da instanciação com variáveis de tipo ............149
8.4 Conclusões ............................................................................................................150
Capítulo 9 – Modos ...............................................................................................................153
9.1 Conceitos e mecanismos de L9 .............................................................................154
9.1.1 Modos .....................................................................................................154
9.1.2 Exemplo: o modo log ..............................................................................155
9.1.2.1 Faceta dinâmica do modo log...................................................156
9.1.2.2 Faceta estática do modo log .....................................................157
xvi
Índice
9.1.3 O que é um modo? .................................................................................158
9.1.4 Implementação dum modo .....................................................................159
9.2 Semântica de L9 ...................................................................................................160
9.2.1 Semântica dos tipos................................................................................160
9.2.2 Semântica dos termos.............................................................................161
9.2.2.1 Modos......................................................................................161
9.2.2.2 Instanciação dum modo ..........................................................162
9.2.2.3 Boa tipificação da equação semântica ....................................164
9.2.3 Operadores de modo ..............................................................................165
9.3 Discussão sobre L9 ...............................................................................................166
9.4 Conclusões ............................................................................................................166
Capítulo 10 – Sistema de coerções ......................................................................................169
10.1 Conceitos e mecanismos de L10.........................................................................169
10.1.1 Coerções e relação de coerção .............................................................170
10.1.2 Sistema de coerções .............................................................................170
10.1.3 Coerções de modo ................................................................................171
10.1.4 Funções de conversão sem redundância ..............................................172
10.2 O sistema natural ................................................................................................174
10.2.1 Apresentação das regras de básicas do sistema natural .......................174
10.2.2 Regras básicas do sistema natural ........................................................175
10.2.3 Regras de coerção extra .......................................................................176
10.2.4 Operadores de conversão .....................................................................177
10.2.5 Regras terminais e árvores de prova ....................................................177
10.2.6 Procedimentos de prova .......................................................................179
10.2.7 Problemas do sistema natural...............................................................181
10.2.7.1 Indeterminismo .....................................................................181
10.2.7.2 Ambiguidade .........................................................................183
10.2.7.3 Indecidibilidade.....................................................................184
10.2.7.3.1 Procedimento geral de prova ..................................184
10.2.7.3.2 Propriedade da subfórmula.....................................185
10.2.7.3.3 Indecidibilidade do sistema natural ........................187
10.3 O sistema prático ................................................................................................190
10.3.1 Apresentação das regras básicas do sistema prático ............................191
10.3.2 Regras básicas do sistema prático ........................................................191
10.3.3 Consequências da eliminação da regra da transitividade .....................192
10.3.4 Procedimentos de prova normalizado e prático ...................................193
10.3.5 Propriedades dos procedimentos de prova ...........................................194
10.3.6 Propriedades do sistema prático...........................................................199
Índice
xvii
Capítulo 11 – Linguagem OM .............................................................................................211
11.1 Sintaxe da linguagem OM...................................................................................211
11.2 Nomeação das classes e tipos-objecto ................................................................212
11.2.1 Regras de nomeação .............................................................................213
11.2.2 Justificação das regras de nomeação ....................................................213
11.2.3 Aspectos práticos..................................................................................214
11.2.4 Exemplo................................................................................................214
11.3 Sobreposição da sintaxe de OM ..........................................................................214
11.3.1 Atribuição de semântica às construções de semântica variável ...........215
11.3.2 Matéria-prima semântica ......................................................................216
11.4 Nível privilegiado e recursos especiais ...............................................................217
11.5 Componentes globalizadas..................................................................................218
11.5.1 Utilidade ...............................................................................................219
11.6 Resolução de nomes ............................................................................................220
11.7 Biblioteca de classes mínima ..............................................................................221
Capítulo 12 – Modos da biblioteca padrão .........................................................................229
12.1 Componentes adicionadas a $CoreObject ..............................................................229
12.2 Modo const ...........................................................................................................230
12.3 Modo value...........................................................................................................231
12.4 Modo lazy ............................................................................................................233
12.5 Modo log ..............................................................................................................235
12.6 Modo gen .............................................................................................................238
12.6.1 Protótipo do modo gen ..........................................................................242
Capítulo 13 – Exemplo .........................................................................................................245
13.1 O problema dos padrões......................................................................................245
13.1.1 Padrões .................................................................................................246
13.1.2 Uso dos padrões....................................................................................247
13.1.3 A classe abstracta Pattern .......................................................................248
13.1.4 As classes concretas .............................................................................249
Conclusões .............................................................................................................................253
Bibliografia ............................................................................................................................255
Capítulo 1
Introdução
“‘OM’ means the first vibration – that sound, that spirit that sets everything else into being. It is The Word from which all men and everything
else comes, including all possible sounds that man can make vocally. It is
the first syllable, the primal word, the word of power."
John Coltrane
1.1 Paradigmas de programação
Um paradigma de programação é um modelo conceptual que determina uma forma particular
de abordar os problemas de programação e de formular as respectivas soluções. Cada paradigma é caracterizado por um conjunto de conceitos mutuamente relacionados, ou seja, por uma
ontologia própria. Por exemplo, o paradigma de programação imperativo é caracterizado pelas
noções de memória, atribuição e sequenciação; o paradigma funcional pelas noções de função
e aplicação; o paradigma lógico pelas noções de relação e dedução lógica.
Diferentes paradigmas de programação representam visões distintas, e muitas vezes irreconciliáveis, do processo de resolução de problemas. Assim, o grau de sucesso de cada
paradigma de programação na resolução dum problema particular pode variar muito. Se num
determinado contexto conceptual esse problema pode ter uma solução natural e fácil de
descobrir, noutro contexto conceptual o problema pode ser de árdua resolução e exigir um
tratamento elaborado e artificial.
Assim se compreende que o grau de sucesso dum programador dependa da colecção de
paradigmas que domine e da sua arte em escolher, para cada problema, o modelo conceptual
mais indicado para tratar esse problema. Citando Robert Floyd na sua “ACM Turing Award
Lecture” intitulada “The Paradigms of Programming” [Flo79]:
“If the advancement of the general art of programming requires the continuing invention and
elaboration of paradigms, advancement of the art of the individual programmer requires that
he expand his repertory of paradigms.”
Diz-se que uma linguagem de programação suporta um dado paradigma de programação se
as construções e mecanismos dessa linguagem reflectirem directamente os conceitos do paradigma. Uma solução elaborada segundo um certo paradigma pode ser expressa directamente
numa linguagem que suporte esse paradigma. Voltando a citar Floyd [Flo79]:
2
OM – Uma linguagem de programação multiparadigma
“I believe the continued advance of programming as a craft requires development and dissemination of languages which support the major paradigms of their user's communities.”
As linguagens modernas tendem a suportar mais do que um paradigma de programação.
Há exemplos em que esse efeito se obtém estendendo uma linguagem já existente: é o caso
do C++ [ES90], que resultou da incorporação, na linguagem C [KR78], de suporte para o paradigma orientado pelos objectos; é também o caso da linguagem HOPE [DFP86], que nasceu
como uma linguagem funcional mas evoluiu assimilando aspectos do paradigma lógico.
Outras linguagens foram desenhadas logo de início com o objectivo de suportar um conjunto alargado de paradigmas. Por exemplo, na proposta inicial do sistema Andorra [Har89]
anuncia-se: “our approach has been to integrate the paradigms of Prolog, committed choice
and process description languages, concurrent objects, and constraint programming in a
single unified framework”. Nesta categoria incluem-se também as linguagens Nial [JG86] e
Leda [Bud95], as quais suportam, com variável grau de sucesso na integração, os paradigmas
imperativo, funcional, lógico e orientado pelos objectos.
Uma linguagem multiparadigma [Pla91] tem um poder expressivo acrescido relativamente
a uma linguagem uniparadigma. Numa linguagem multiparadigma a paleta conceptual à disposição do programador é mais vasta e, se a linguagem estiver bem articulada, criam-se sinergias
que fazem com que cada paradigma recolha benefícios da presença dos outros.
Para esclarecer um possível equívoco, o conceito de poder expressivo [Fel90] não deve ser
confundido com o conceito de poder computacional: repare que do ponto de vista do poder
computacional, a generalidade das linguagens de programação são universais [Chu36], logo
computacionalmente equivalentes entre si.
As principais questões específicas que surgem no contexto do estudo duma linguagem
multiparadigma são os problemas da sua consistência e clareza semântica, e também a determinação de qual a melhor sintaxe para representar conceitos e mecanismos, por vezes tão dispares.
1.2 Linguagens extensíveis
Consideremos uma linguagem de programação universal, digamos a linguagem Pascal e imaginemos uma sua versão estendida, chamada Pascal +, por exemplo. Para definir a sintaxe e semântica da nova linguagem, existem diversas técnicas genéricas conhecidas: gramáticas, o método operacional, o método axiomático, etc. Mas esqueçamos estas técnicas gerais e foquemos
a nossa atenção na possibilidade de definir a semântica da linguagem Pascal+ usando a própria
linguagem Pascal: afinal, sendo universal e estando bem definida, a linguagem Pascal deverá
também poder ser usada como veículo de especificação sintáctica e semântica. Existem pelo
menos duas soluções para este problema.
1 Introdução
3
A primeira solução consiste em escrever em Pascal um programa tradutor que aceite como
entrada qualquer programa escrito em Pascal + e produza como resultado um programa com o
mesmo significado, mas agora totalmente reescrito em Pascal. Este programa tradutor define
efectivamente a sintaxe e a semântica da linguagem Pascal+, além de constituir também uma
implementação da linguagem estendida.
A segunda solução consiste em escrever em Pascal um interpretador de Pascal+, um programa que valide os aspectos estáticos dos programas Pascal + aos quais seja aplicado e que
preceda seguidamente à sua execução. Também neste caso, estamos perante um programa
escrito em Pascal que, de forma efectiva, especifica a sintaxe e a semântica da linguagem
Pascal+.
No primeiro caso, dizemos que foi usada uma técnica de definição estática; no segundo
caso, foi usada uma técnica de definição dinâmica. São duas técnicas distintas, mas igualmente
eficazes. (No que diz respeito ao segundo caso, é interessante considerar a situação particular
Pascal=Pascal+: nesta situação, parece que a linguagem Pascal se define a ela própria, através
do que se convencionou chamar um interpretador metacircular. Mas não há aqui qualquer
mistério, já que foi assumido que a linguagem Pascal se encontrava completamente definida à
partida.)
Esta discussão pretende acima de tudo mostrar que existe um potencial que pode ser explorado para a definição de linguagens extensíveis: basta que a linguagem inclua algum mecanismo que permita interiorizar os procedimentos de tradução e interpretação externos, atrás descritos.
Ao longo da história das linguagens de programação têm sido propostos diversos destes
mecanismos. Um dos mais antigos é certamente o próprio mecanismo dos procedimentos:
através da definição de procedimentos é possível enriquecer o conjunto de operações disponíveis para serem usadas nos programas, sendo as novas operações definidas usando a própria
linguagem, como se sabe. Outro mecanismo antigo é o das syntax-macros [Lea66], que
Leavenworth apresenta como sendo um mecanismo que permite estender a sintaxe e a semântica duma linguagem de alto-nível: trata-se aproximadamente da ideia que está na base do pré-processador da linguagem C.
Estes dois mecanismos são de natureza estática. Vejamos agora dois exemplos de natureza
dinâmica. A linguagem Lisp [Mac62] adopta uma sintaxe que não estabelece distinção entre
dados e programas. Este aspecto facilita a escrita de programas que manipulam outros programas e, em particular, facilita a escrita de interpretadores que definam semânticas alternativas
para a linguagem. Outro exemplo é o sistema CLOS [DG87], que inclui uma componente, o
“CLOS Metaobject Protocol” [KRB91], que oferece uma implementação metacircular do próprio sistema CLOS. Este sistema admite a modificação dos mecanismos básicos da linguagem,
e.g. envio de mensagem, a partir do interior da própria linguagem e, inclusivamente, de forma
dinâmica.
4
OM – Uma linguagem de programação multiparadigma
1.3 Apresentação da linguagem e do trabalho
Este trabalho explora a ideia da criação duma linguagem multiparadigma e estaticamente tipificada, na qual os paradigmas suportados sejam integrados, não através da introdução directa e
ad-hoc de novas construções e mecanismos primitivos, mas antes por meio dum mecanismo
de extensão uniforme, obedecendo a princípios claros e bem definidos. Esse mecanismo de extensão deverá ser simples, preferencialmente de natureza estática ou semi-estática, e basear-se
em conceitos estabelecidos, dentro do possível.
Foi assim criada a linguagem OM, uma estrutura linguística semanticamente extensível,
com a capacidade de crescer por adição de conceitos e mecanismos de diferentes paradigmas.
O núcleo da linguagem OM apresenta-se sob a forma duma linguagem orientada pelos objectos/imperativa, baseada em classes. Esta escolha não foi casual. Em primeiro lugar, como
contraponto à extensibilidade da linguagem, é importante que o núcleo da linguagem imponha
formas de organização dos programas, fixando todos os aspectos de programação em grande.
Em segundo lugar, a generalidade das linguagens baseadas em classes incorpora já alguns aspectos de extensibilidade que foram por nós explorados de forma essencial: note que uma classe é uma entidade extensível, por natureza.
O núcleo da linguagem suporta ainda um mecanismo de programação de nível meta, designado por modo ou mecanismo dos modos, que é o mecanismo de extensão que propomos para a linguagem. O mecanismo dos modos actua na linguagem através da alteração da funcionalidade das entidades tipificadas da linguagem, concretamente dos objectos, variáveis, expressões, parâmetros de função e resultados de função. Para ilustrar a influência dos modos sobre
as variáveis, por exemplo, apresentamos dois exemplos simples: uma variável inteira com modo lógico (declarada com tipo “log Int”) tem a funcionalidade das variáveis simbólicas da linguagem Prolog [CM81, Hog84]; uma variável inteira com modo constante (declarada com tipo
“const Int”) é obrigatoriamente inicializada no ponto da declaração e não pode ser alterada.
Na linguagem OM é possível introduzir um número ilimitado de modos, especificando cada modo um pacote de conceitos e mecanismos interligados. Um modo define-se usando a própria linguagem OM através duma construção sintáctica parecida com uma classe paramétrica.
No mecanismo dos modos há duas facetas a considerar: uma dinâmica e outra estática. A
faceta dinâmica concentra-se na questão do enriquecimento da funcionalidade dinâmica dos
objectos com modo, a qual é especificada por meio duma implementação. A faceta estática
concentra-se nas propriedades estáticas das entidades tipificadas com modo, propriedades que
são especificadas com recurso a um sistema de coerções extensível e a um esquema simples de
sobreposição de sintaxe
A biblioteca padrão da linguagem OM inclui cinco modos, já definidos, que ilustram bem
as possibilidades do mecanismo dos modos. São eles: o modo const, que introduz constantes na
linguagem; o modo value, que introduz semântica de não-partilha; o modo lazy, que introduz
1 Introdução
5
uma variante de call-by-name e a possibilidade de trabalhar com estruturas de dados infinitas;
o modo log, que introduz variáveis lógicas e unificação sintáctica e semântica; o modo gen,
que pela via da noção de gerador, introduz retrocesso (backtracking) na linguagem.
A parte mais substancial desta tese consiste na construção dum modelo teórico para uma
versão abstracta da linguagem OM que será referida por linguagem L10: o modelo define rigorosamente esta linguagem, funcionando como seu suporte explicativo. No modelo consideram-se os seguintes elementos: objectos, classes, herança, polimorfismo paramétrico, ocultação de
informação, estado, componentes de classe e a faceta dinâmica dos modos. Como a formalização do mecanismo dos modos requer o envolvimento, directo ou indirecto, de todos os outros
elementos do modelo, a introdução dos modos representa o culminar do processo de desenvolvimento do modelo.
Reflectindo o nosso objectivo de criar uma linguagem estaticamente tipificada, foi um modelo tipificado aquele que desenvolvemos para a linguagem. Ao longo da última década muitos investigadores têm tentado superar as muitas dificuldades envolvidas na criação de sistemas de tipos estáticos para linguagens orientadas pelos objectos que não interfiram excessivamente na expressividade dessas linguagens [FM95]. Neste aspecto, o nosso trabalho assimila o
presente estado da arte, e melhora-o em alguns aspectos pontuais (referidos na secção 1.5).
Esta tese inclui ainda: o desenvolvimento dum sistema de coerções extensível para suporte
da faceta estática dos modos; a definição da linguagem concreta OM com base na linguagem
abstracta definida pelo modelo; a definição duma biblioteca padrão contendo diversas classes e
modos predefinidos; finalmente, um exemplo que ilustra uma aplicação prática e não trivial da
linguagem OM.
1.4 Estrutura da dissertação
A presente dissertação está organizada da seguinte forma.
O capítulo 2 introduz um cálculo-lambda polimórfico de ordem superior que se destina a
ser usado nos capítulos seguintes como veículo da descrição semântica da linguagem OM.
Este cálculo, designado por F+, agrupa num todo coerente elementos extraídos de diferentes
fontes da literatura e ainda alguns elementos simples da nossa responsabilidade. O capítulo 2 é
um pouco extenso porque nele se tenta antecipar a satisfação de todas as necessidades futuras
relacionadas com o sistema F+.
O capítulo 3 introduz a linguagem L3, uma linguagem simples que não é mais do que um
ponto de partida para a introdução gradual dos vários elementos da linguagem OM.
O capítulo 4 introduz a linguagem L4, uma linguagem orientada pelos objectos com um
sistema de tipos estático rudimentar, semelhante aos sistemas de tipos de linguagens como o
C++ ou o Java. L4 partilha com estas duas linguagens a característica favorável de todas as
suas subclasses serem geradoras de subtipos, relativamente ao tipo gerado por cada superclas-
6
OM – Uma linguagem de programação multiparadigma
se. Neste capítulo, apresentamos os conceitos essenciais da linguagem L4, fazemos o seu desenvolvimento formal (com base num modelo monomórfico), e discutimos as fragilidades do
seu sistema de tipos, entre outros aspectos.
O capítulo 5 introduz a linguagem L5, uma evolução da linguagem L4 que suporta o nome
de tipo SAMET, um nome de tipo genérico que dentro de cada classe representa o tipo externo
de self . Diversos modelos teóricos da literatura suportam variantes de SAMET, sendo bem conhecido o impacto da sua introdução. Na linguagem L5, a introdução do tipo SAMET permite
contornar algumas das limitações de expressividade que o sistema de tipos de L4 determina:
em particular são alargadas as modalidades de reutilização de código. No entanto, a introdução
de SAMET cria um problema: em certas situações as subclasses deixam de gerar subtipos, o que
prejudica a aplicação de certas técnicas de programação genérica que pressupõem a existência
de subtipos. Dentro da filosofia multiparadigma da linguagem OM, “restrições arbitrárias de
expressividade não são aceitáveis”, procurámos e encontrámos uma solução para o problema.
Trabalhando com uma relação de extensão entre classes mais fraca do que é habitual, foi possível introduzir um transformador de classes “ +” que possibilita a geração de versões não problemáticas de classes problemáticas.
O capítulo 6 introduz a linguagem L6, que estende L5 com uma forma de polimorfismo paramétrico restringido em que a restrição sobre os tipos-parâmetro é imposta usando a mesma
relação binária que limita o tipo gerado por uma subclasse, face à interface da sua superclasse.
Assim, esta forma de polimorfismo paramétrico permite operar de forma genérica sobre todos
os objectos gerados pelas subclasses duma dada classe (mesmo que essas subclasses não gerem subtipos). Polimorfismo paramétrico é um mecanismo de programação genérica de base
estática que contribui de forma essencial para a expressividade de qualquer linguagem.
O capítulo 7 é dedicado às questões da ocultação de informação e do estado. As regras de
encapsulamento da linguagem L7 baseiam-se nas regras de encapsulamento da linguagem
Smalltalk. No entanto, relaxamos estas regras um pouco, na fase inicial de vida dos objectos,
para permitir que estes sejam inicializados por entidades exteriores. O nosso modelo mostra
em que condições é possível fazer isso.
O capítulo 8 define a linguagem L8, na qual introduzimos componentes de classe e classes
recursivas sobre o nome abstracto SELFC. As componentes de classe constituem um mecanismo de utilidade geral: permitem definir construtores, definir variáveis partilhadas, e exprimir
informação logicamente associada a cada classe; são ainda exploradas na definição da parte
estática do mecanismo dos modos. Neste capítulo introduzimos ainda uma outra forma de polimorfismo paramétrico – polimorfismo de classe – que será a forma exclusiva de polimorfismo paramétrico a adoptar na linguagem concreta final OM.
O capítulo 9 introduz a linguagem L9. É neste capítulo que se apresenta e formaliza a noção de modo, ou mais exactamente a faceta dinâmica da noção de modo. O mecanismo dos
modos é o mecanismo de extensão semântica que está na base das características multipara-
1 Introdução
7
digma da linguagem OM. Actua influenciando de forma uniforme a funcionalidade das entidades tipificadas da linguagem.
O capítulo 10 é dedicado ao desenvolvimento do sistema de coerções extensível da linguagem L10. São estudadas duas versões deste sistema. A versão final, designada sistema prático,
é decidível. O sistema prático recorre a um algoritmo de prova completo e determinista, chamado de procedimento de prova prático, que incorpora uma exigência especial do mecanismo
dos modos: a geração de árvores de prova com tamanho mínimo.
O capítulo 11 descreve a linguagem prática OM e a sua biblioteca de classes, dita biblioteca mínima. A linguagem prática alarga os mecanismos de especificação da faceta estática
dos modos, estabelece diversos aspectos pragmáticos (uma sintaxe concreta, regras de nomeação de classes e tipos, e regras de resolução de nomes) e adquire um nível privilegiado no contexto do qual a própria linguagem pode ser estendida ou alterada.
O capítulo 12 descreve os cinco modos da biblioteca padrão da linguagem OM.
O capítulo 13 discute a resolução, usando a linguagem OM, dum problema não trivial, escolhido para ilustrar a acção combinada dos paradigmas suportados.
1.5 Principais contribuições desta tese
No conhecido trabalho "Inheritance is not subtyping" [CHC90], mostra-se que quando se aumenta a flexibilidade dum modelo de objectos para exprimir herança, a utilidade da relação de
subtipo se reduz significativamente. Isto acontece porque se perde a garantia de que as subclasses da linguagem sejam geradoras de subtipos. Esta circunstância levanta sérios problemas
práticos de expressividade que, paradoxalmente, são quase ignorados na literatura. A questão é
abordada por Bruce em [BPF97], mas a solução encontrada envolve a eliminação da relação
de subtipo e a sua substituição por uma relação alternativa menos satisfatória.
A primeira contribuição importante desta tese consiste numa solução para o conflito entre o
mecanismo de herança e a relação de subtipo, atrás apresentado. Efectuamos a análise do problema no contexto duma relação de herança muito geral e complexa (só com interesse teórico),
uma relação suficientemente rica para permitir a descoberta duma solução; depois simplificamos a relação tendo o cuidado de não prejudicar a validade da solução. No contexto desta tese,
esta é uma questão importante pois pretendemos evitar que a linguagem OM, uma linguagem
dita multiparadigma, seja incapaz de suportar os principais idiomas de programação usados
nas linguagens C++, Java, e Smalltalk.
A segunda contribuição importante consiste na noção de modo e da sua explicação por
meio duma formalização. Acreditamos que a nossa noção de modo se encontra no ponto ideal
entre a simplicidade, naturalidade e capacidade de introduzir uma dimensão de extensibilidade
numa linguagem. Esta noção resultou dum longo processo de experimentação e amadurecimento. Ela foi ganhando diversas formas ao longo do tempo: mas as várias alternativas que se
8
OM – Uma linguagem de programação multiparadigma
iam apresentando eram, todas elas, ou demasiado complicadas de usar, ou demasiado complicadas de explicar, ou então possuíam problemas funcionais. Relativamente à versão final foram duas as razões que nos convenceram de que teríamos encontrado uma solução razoável: a
compacidade e a razoabilidade da formulação da sua faceta dinâmica, e o facto dos aspectos
estáticos dos modos poderem ser explicados usando as noções estabelecidas de coerção e sobreposição de sintaxe.
As duas contribuições atrás descritas são as mais importantes desta tese. Como contribuições menores podemos referir:
• A solução pragmática encontrada para resolver os problemas da indecidibilidade e da
ambiguidade no nosso sistema de coerções extensível;
• A arquitectura da linguagem concreta final e sua biblioteca padrão, que julgamos serem
simples e eficazes (também resultaram dum longo processo de convergência, possivelmente ainda não concluído);
• O modelo semântico desenvolvido, que tem a virtualidade de cobrir de forma coerente,
organizada e bastante abordável um largo espectro de mecanismos orientados pelos objectos. O modelo combina, adaptando, diversas ideias da literatura, com algumas nossas.
Capítulo 2
O Sistema F+
Neste capítulo, introduzimos um cálculo-lambda polimórfico de ordem superior que usaremos
ao longo da presente tese como modelo de fundação para a linguagem OM. Designamos este
cálculo por sistema F +. Também antecipamos, neste capítulo, a resolução e discussão de todas
as necessidades futuras relacionadas com o sistema F+ que possam ser tratadas no âmbito deste
sistema, sem necessidade de contexto suplementar.
O sistema F+ tem por base o sistema Fω de Girard [Gir72, SP94], com a seguinte lista de ingredientes adicionados: relação de subtipo [CW85, CG92, CMMS94], quantificação universal
F-restringida (F-bounded) [CCH+89, CHC90], quantificação existencial F-restringida, tipos
recursivos [AC93, AF96], tipos-registo simples (não extensíveis), pares ordenados de tipos e
pares ordenados de termos.
Os ingredientes que integram o sistema F+ aproximam-se dos ingredientes usados noutros
modelos tipificados para linguagens de objectos, nomeadamente nos modelos descritos em
[Car88, CW85, CHC90, ESTZ94, Bru94, PT94, ACV96, BSG95, BFSG98]. O sistema F+ limita-se a concretizar algumas escolhas que se pretendem adaptadas à linguagem a formalizar.
Por exemplo, o mecanismo de herança de OM requer o uso de quantificação universal F-restringida [CCH+89], enquanto que a generalidade dos modelos referidos usa, ou poderia usar,
apenas quantificação universal restringida de ordem superior [AC96b].
Não pretendemos desenvolver neste capítulo a meta-teoria de F+, nem criar um modelo semântico para este sistema. Não obstante, como referência orientadora, adoptámos um modelo
semântico conhecido da literatura: o modelo de Bruce e Mitchell [BM92], um modelo abstracto muito geral e com um largo espectro de aplicação. Em conformidade, integrámos em F +
apenas ingredientes claramente suportados por este modelo.
Outros critérios usados no estabelecimento de F+ foram: a expressividade, a naturalidade e
a generalidade. Relativamente à expressividade, o sistema F+ tem evidentemente de ser suficientemente expressivo para permitir a codificação de todas as construções da linguagem OM.
Quanto à naturalidade, trata-se de incluir no sistema os elementos certos, que simplifiquem e
tornem mais intuitiva a codificação de OM em termos de F +. Demos prioridade a este factor,
mesmo em detrimento do minimalismo do sistema: por exemplo, seria possível evitar usar
tipos recursivos, como faz Pierce no seu “modelo existencial” [PT94], mas isso exigiria um
tratamento menos simples e menos directo de alguns aspectos da linguagem OM. Finalmente,
10
OM – Uma linguagem de programação multiparadigma
quanto à generalidade, escolhemos sempre as formulações mais gerais incluídas na literatura e
admitidas pelo modelo semântico referido no parágrafo anterior. Por exemplo, no caso dos
tipos recursivos, das várias formulações disponíveis [AF96] escolhemos a formulação de
Amadi e Cardelli [AC96b] que trata um tipo recursivo como sendo equivalente à sua própria
expansão infinita (o que é compatível com o facto do modelo semântico prever soluções
mínimas para todas as equações de tipo da forma F(A)=A ). Também no caso da quantificação
universal, nos baseámos na formulação de Ghelli [CG92], que adaptarmos ao contexto do
polimorfismo F-restringido, em vez de partirmos da formulação original, mas menos geral, de
Cardelli [CW85].
Os objectos da linguagem OM são codificados em F+ como registos recursivos de tipo
também recursivo. As classes são definidas usando quantificação universal F-restringida e o
mecanismo de herança é definido usando uma relação entre operadores de tipo que, indirectamente, se baseia na relação de subtipo.
Na literatura, são apresentados outros modelos tipificados para linguagens orientadas pelos
objectos, baseados em técnicas bem diferentes da que adoptamos, nomeadamente: registos
extensíveis [Wan87, Wan89, Rém89, Mit90, CM91, Car94], cálculo de objectos primitivos
[FHM94, AC94, ACV96], método denotacional directo [Bru94], método operacional directo
[BSG95, BFSG98].
2.1 O sistema F e suas extensões
O sistema F e muitas das suas extensões-padrão estão na base do sistema F+. Por isso nada melhor do que apresentar estes sistemas, mesmo que com alguma brevidade, como forma de criar
um contexto propício ao melhor entendimento de F+.
2.1.1 O sistema F
O sistema F [Gir71, Rey74] é um calculo-lambda de segunda ordem que, para além da abstracção de valor, λx:τ.e, e aplicação, (f e), típicos do cálculo-lambda tipificado de primeira ordem
[Chu40], inclui uma forma de abstracção de tipo, λX.e, e a correspondente operação de aplicação (ou instanciação), F[τ] . As abstracções de tipo da forma λX.e chamam-se funções polimórficas e representam funções de tipos para valores. Na abstracção λX.e, tanto o termo e como o
seu tipo estão parametrizados relativamente à variável de tipo X . O tipo de λX.e denota-se
∀ X.τ , onde τ é o tipo de e. Para exemplificar, a função polimórfica identidade pode escrever-se
como λX.λx:X.x , e tem tipo ∀X.X→X.
O sistema F, assim como as suas variantes F ω e F≤, que iremos referir nos pontos seguintes,
satisfazem as propriedades de confluência e da normalização forte. Portanto, todos os termos
são redutíveis a uma forma normal, que é independente das escolhas dos subtermos a reduzir,
ao longo do processo de redução.
2 O sistema F+
11
O sistema F diz-se impredicativo (ou circular) pois uma função polimórfica pode ser aplicada a um tipo qualquer, inclusivamente ao seu próprio tipo, como exemplificamos aplicando
a identidade polimórfica ao seu próprio tipo: (λX.λx:X.x)[∀ X.X→X] .
O sistema F diz-se paramétrico porque foi concebido para suportar exclusivamente funções
polimórficas paramétricas. Uma função polimórfica diz-se paramétrica se o seu comportamento genérico não depender do tipo usado na sua instanciação [Str67]. Por outras palavras,
uma função polimórfica diz-se paramétrica se for possível escrevê-la duma forma que não dependa do tipo dos seus parâmetros. Por exemplo, é certamente paramétrica uma função polimórfica dedicada à determinação do comprimento de listas, pois esta determinação não requer
a consideração do tipo dos elementos das listas.
Um princípio geral, aplicável a todas as linguagens paramétricas, é o seguinte: A informação de tipo que ocorre nos programas é usada em tempo de compilação, mas é ignorada em
tempo de execução.
Um resultado clássico que advém da parametricidade do sistema F, indica que o tipo paramétrico ∀X.X→X contém como único elemento, a identidade polimórfica λX.λx:X.x . É fácil verificar este facto intuitivamente: se P for uma função de tipo ∀ X.X→X e se instanciarmos X
com um conjunto singular, {a}, descobrimos que P[{a}] tem de ser a função identidade sobre
{a}; mas como f[X] opera independentemente do tipo X, então P[X] tem ser a identidade sobre
X, independentemente do tipo X.
A natureza do conceito de parametricidade é essencialmente semântica, pelo que não deve
surpreender que a teoria sintáctica de F não consiga capturar este conceito. Em particular, não
é possível demonstrar o resultado anterior usando apenas a teoria sintáctica do sistema F.
Em 1983, Reynolds formalizou pela primeira vez, e por via semântica, uma noção de parametricidade para uma linguagem semelhante ao sistema F [Rey83]. Reynolds conseguiu capturar o carácter paramétrico da sua linguagem removendo do modelo semântico toda a informação de tipo associada aos termos a interpretar. No seguimento do trabalho de Reynolds surgiram outros estudos dentro da mesma linha [Wad89, BL90, MR91, BM92]. Mais recentemente,
surgiram abordagens sintácticas da parametricidade, que passam por complicar a teoria sintáctica associada aos sistemas em estudo [ACC93, CMMS94].
Todas as variantes do sistema F estudadas neste ponto e nos pontos que se seguem são
paramétricas. Para verificar este facto basta considerar que o modelo de [BM92] (cf. secção
2.6) as captura todas.
2.1.2 O sistema Fω
O sistema Fω [Gir72, SP94] estende o sistema F com operadores de tipo, os quais se destinam
a modelizar tipos parametrizados (ou seja, funções de tipos para tipos). Este sistema define um
cálculo-lambda tipificado de primeira ordem ao nível dos próprios tipos (possuindo a sua pró-
12
OM – Uma linguagem de programação multiparadigma
pria regra de redução-β) e introduz regras de boa-formação dos operadores de tipo usando um
meta-sistema de tipos.
Os meta-tipos são designados por géneros (kinds). O género dos tipos simples (tipos não
parametrizados) é denotado por ∗; o género dos operadores de tipo que geram tipos simples
quando aplicados a tipos simples é denotado por ∗⇒∗; a expressão (∗⇒∗)⇒∗ denota o género de
todos os operadores de tipo que geram tipos simples quando aplicados a operadores de tipo do
género ∗⇒∗, etc.
A forma sintáctica geral dum operador de tipo é ΛX:Κ.κ , onde X é uma variável de tipo do
género Κ, e κ é um género no qual a variável X pode ocorrer. Este operador é do género Κ⇒Κ′,
se Κ′ for o género de κ . Para dar um exemplo, o operador de tipo identidade sobre tipos simples escreve-se Λ X:∗.X e pertence ao género ∗⇒∗.
O facto de existir uma regra de redução-β associada aos tipos torna a relação de identidade
= entre tipos não trivial, pelo que esta deve ser formalizada. Por exemplo, a seguinte identidade de tipos deverá ser válida em todo o contexto: (λX:Κ.κ′)κ=κ′[κ/X].
Ao contrário do sistema F, o sistema Fω é predicativo pois nele os tipos estão estratificados
em universos disjuntos. Em Fω não é possível definir uma função identidade tão geral como a
apresentada na secção anterior; quanto muito é possível definir uma identidade idΚ para cada
género Κ, pertencendo, nesse caso, o tipo da identidade idΚ a um género diferente de Κ, concretamente ao género Κ⇒Κ.
O sistema F ω satisfaz as propriedades da confluência e da normalização forte. Em particular, a sublinguagem dos tipos satisfaz estas propriedades o que nos permite aplicar a regra de
redução-β para tipos com toda a liberdade (isto é, a ordem das aplicações não influencia a forma normal final, a qual garantidamente existe).
2.1.3 Os sistemas F≤ e Fω≤
O sistema F≤ [CW85, CG92] enriquece F com uma relação binária de subtipo ≤ e com uma nova forma de polimorfismo paramétrico no qual as variáveis abstraídas estão sujeitas a um limite superior. As novas abstracções têm a forma geral λX≤B.e e tipo ∀ X≤B.τ, onde τ é o tipo de e.
O sistema F≤ permite escrever funções aplicáveis a todos os subtipos dum tipo particular. Para
garantir que o sistema F pode ser mergulhado no sistema F≤ adiciona-se um elemento máximo
Top aos tipos de F ≤. Assim os termos λX.e do sistema F são reintroduzidos em F≤ sob a forma
λX≤Top.e.
A autoria do sistema F≤ é atribuída a Cardelli. Este sistema foi motivado pelo estudo de
modelos para linguagens orientadas pelos objectos. O cálculo original (de Cardelli e Wegner
[CW85]) chamava-se “Fun” e incluia o sistema F≤ como um fragmento. O sistema foi depois
desenvolvido em [Car88, Car90, CMMS94] e ainda por Curien e Ghelli em [CG92].
2 O sistema F+
13
Na variante padrão do sistema F≤ [CG92], a relação de subtipo prova-se indecidível [Pie93,
Ghe93].
ω
O sistema Fω
≤ resulta da extensão de F com: uma relação de subtipo introduzida ao nível
de cada género Κ, novas abstracções da forma λX≤B.e e um elemento máximo Top(Κ) ao nível
de cada género. A meta-teoria duma variante de F ω
≤ é extensamente desenvolvida em [SP94].
2.1.4 Polimorfismo paramétrico F-restringido
Se generalizarmos o sistema F≤ por forma a permitirmos que, nas abstracções de tipo, a variável de tipo ocorra no seu próprio limite superior, como em λX≤F[X].e, então obtemos uma forma de polimorfismo paramétrico designada por polimorfismo paramétrico F-restringido
[CCH+89]. No termo “F-restringido”, a letra “F” serve para indicar que o limite superior do
tipo do parâmetro da abstracção é baseado numa função F de tipos para tipos. O tipo da abstracção λX≤ϕ[X].e escreve-se ∀X≤ϕ[X].τ , onde τ é o tipo de e.
Note que o polimorfismo F-restringido é uma combinação de recursão com polimorfismo
de F ≤ pois o parâmetro é parcialmente caracterizado por um limite superior no qual o próprio
parâmetro pode ocorrer. Se o operador de tipo ϕ for um tipo-registo parametrizado, então a
condição X≤ϕ[X] estabelece que X deve ser um tipo-registo contendo as componentes de ϕ[X],
pelo menos. Ora essas componentes dependem de X como se pode observar. Assim, um tipo τ
que verifique a condição τ≤ϕ[τ] será muitas vezes um tipo recursivo. No mesmo sentido, note
que a equação de tipos X=ϕ[X] define um tipo recursivo, e que a inequação X≤ϕ[X] resulta dum
enfraquecimento daquela equação.
Um exemplo bem ilustrativo das possibilidades expressivas do polimorfismo paramétrico
F-restringido é a seguinte função comp:
comp
=ˆ λX≤{eq:X→Bool}.(λx:X.λy:X.(x.eq y))
Esta função pode ser instanciada com um tipo registo X qualquer, desde que contenha uma
operação eq que permita a um elemento doe tipo X comparar-se com qualquer outro elemento
do tipo X. Quanto à função comp, esta compara dois valores genéricos, x e y, do tipo X, usando
a operação eq definida em x. Para exemplificar uma invocação de comp, tomemos o tipo
recursivo ρ:
ρ
=ˆ µSAMET.{a:Nat, b:Nat, eq:SAMET→Bool}
e os dois valores recursivos r e s do tipo ρ :
r
s
=ˆ rec self.{a=2, b=3, eq=λx:ρ.(self.a=x.a & self.b=x.b)}
=ˆ rec self.{a=5, b=7, eq=λx:ρ.(self.a=x.a & self.b=x.b)}
A expressão seguinte compara estes dois valores, r e s, usando a operação de igualdade
definida em r:
comp[ρ] r s
14
OM – Uma linguagem de programação multiparadigma
Note que é simples mostrar que ρ verifica a restrição X≤{eq:X→Bool} . Para isso, basta substituir, na inequação, X por ρ e mostrar que ρ≤{eq:ρ→Bool} fazendo uma vez unfolding da ocorrência de ρ do lado esquerdo.
O polimorfismo paramétrico F-restringido foi descoberto por Canning, Cook, Hill, Olthoff
e Mitchell [CCH+89] ao investigarem a forma de ultrapassar as limitações de F≤ na modelização de aspectos das linguagens orientadas pelos objectos. As limitações de F≤ só se tornam
aparentes perante objectos de tipo recursivo (que são afinal os mais comuns).
2.2 Sintaxe de F+
Nesta secção apresentamos a sintaxe de F+, um cálculo-lambda polimórfico de ordem superior
por nós proposto, e que estende o sistema Fω
≤ com polimorfismo paramétrico F-restringido e
mais alguns ingredientes padrão, como sejam tipos recursivos e tipos-registo.
A gramática independente do contexto que iremos apresentar, descreve a sintaxe (livre de
contexto) dos géneros, pré-tipos e pré-termos de F+. A caracterização precisa dos tipos (i.e.
pré-tipos bem formados), e termos (i.e. pré-termos bem formados) envolve aspectos contextuais que serão capturados no sistema de tipos apresentado na secção 2.3.
Consideremos primeiro a linguagem dos géneros (ou meta-tipos). Este conjunto de expressões é definido indutivamente: o seu elemento mais simples, denotado por “∗”, é o conjunto de
todos os tipos da linguagem que não são operadores de tipo nem produtos de tipos. Além disso, se Κ , Κ′ forem géneros, então também serão géneros Κ⇒K′ e Κ×Κ′. “ Κ⇒K′” denota o conjunto de todos os operadores de tipo que aplicam Κ em Κ′ e “Κ×Κ′” denota o produto cartesiano
de Κ e Κ′. Esta classificação dos géneros de F+ está de acordo com o modelo semântico de
[BM92], discutido na secção 2.6, no qual as colecções ℜ e ℑ, definidas indutivamente, modelizam ∗ e Κ , respectivamente.
Κ ::=
∗
| Κ⇒Κ′
| Κ×Κ′
género dos tipos
género dos operadores
género dos produtos
A linguagem dos pré-tipos é descrita pela gramática que se segue. Cada linha da gramática,
exceptuando a primeira, descreve uma forma distinta de pré-tipo. Não fazendo parte da gramática, indicamos para cada forma de pré-tipo qual a forma geral do género a que pertence. As
variáveis usadas na gramática obedecem às seguintes convenções de tipo e género: κ:Κ ,
ϕ:Κ⇒Κ′, τ:∗, υ:∗, σ:∗.
κϕτυσ ::=
X
| TOP(Κ)
| ΛX:Κ.κ
| ϕ[k]
| <κ,κ′>
:Κ
:Κ
:Κ⇒Κ
:Κ
:Κ×Κ′
variável de tipo
tipo máximo do género Κ
operador de tipo
aplicação de operador de tipo
par ordenado de tipos
2 O sistema F+
|
|
|
|
|
κ.n (n=1,2)
µX:Κ.κ
∀ X≤ϕ[X].τ
υ→τ
––
{l :τ}
15
:Κ
:Κ
:∗
:∗
:∗
(Formas derivadas)
| ΛX.κ
:∗⇒Κ
| µX.κ
:∗
| LET X:Κ=κ IN e
:τ
| LET REC <X 1 ,…,Xn >:Κ 1 ×…×Κn =κ IN e:τ
| ∃ X≤ϕ[X].τ
:∗
| τ×τ′
:∗
––
– –
| {l :τ}⊕{l′:τ′}
:∗
tipo projecção
tipo recursivo
tipo universal F-restringido
tipo função
––
tipo-registo ({l :τ} abrevia {l1 :τ1 ‚…‚ln :τn })
operador de tipo sobre ∗
tipo recursivo sobre ∗
declaração local de tipo
declaração de tipos mutuamente recursivos
tipo existencial F-restringido
tipo produto
tipo concatenação
As duas primeiras formas derivadas de termos que ocorrem na gramática têm as seguintes
codificações triviais à custa das formas primitivas do sistema:
ΛX.κ =ˆ ΛX:∗.κ
µX.κ =ˆ µX:∗.κ
As restantes formas derivadas têm codificações mais complexas e serão apresentadas na secção 2.5.
A linguagem das pré-termos é descrita pela gramática que se segue. Cada linha desta gramática descreve uma forma distinta de pré-termo. Não fazendo parte da gramática, indicamos
para cada forma de pré-termo a forma geral do tipo a que pertence. As variáveis que ocorrem
na gramática obedecem às seguintes convenções de tipo: e:τ ‚ f:υ→τ‚ P:∀ X≤ϕ[X].τ.
efP ::=
x
| λx:υ.e
| fe
| λX≤ϕ[X].e
| P[κ]
– –
| {l =e}
| e.l
:τ
:υ→τ
:τ
:∀X≤ϕ[X].τ
:τ[κ/X]
––
:{l : τ}
:τ
variável
abstracção de valor (função simples)
aplicação
abstracção de tipo
aplicação de abstracção de tipo
– –
registo ({l =e} abrevia {l1 =e1 ‚…‚ln =en })
selecção
(Formas derivadas)
| fix
:(τ→τ)→τ
operador de ponto fixo
| rec x:τ.e
:τ
valor recursivo
| let x:υ=e in e′
:τ
declaração local de valor
| let rec <x1 ,…, x n >:υ1 ×…×υ n =e in e′ :τ
declaração de valores mutuamente recursivos
| pack X≤ϕ[X]=σ with e
:∃X≤ϕ[X].τ
criação de pacote
| open e as X,e′ in e′′
:υ
uso de pacote
| <e,e′>
:τ×τ′
par ordenado
| e.n (n=1,2)
:τ
projecção
–– – –
–– – –
––
– –
| +[{l :τ}‚{l′:τ′}]
:{l :τ}×{l′:τ′}→{l :τ}⊕{l′:τ′}
concatenação assimétrica
Para efeitos de levantamento da ambiguidade, nas três gramáticas anteriores consideramos
que os operadores ×, ⊕, + e . são associativos à esquerda e os operadores ⇒ e → são associativos à direita. O âmbito de Λ, µ, ∀, ∃ e λ estende-se tanto quanto possível para a direita.
16
OM – Uma linguagem de programação multiparadigma
Todas as formas derivadas de termos, incluídas na parte final da gramática, serão apresentadas na secção 2.5.
Note que no sistema F+ existem três formas de abstracção: abstracções monomórficas λx:υ.e
(funções de valores para valores), abstracções polimórficas λX≤ϕ[X].e (funções de tipos para
valores), e operadores de tipo ΛX:Κ.κ (funções de tipos para tipos). Para cada uma destas formas de abstracção existe uma forma distinta de aplicação. Repare ainda que existem duas categorias distintas de pares ordenados: pares ordenados de tipos e pares ordenados de termos. Para cada uma destas categorias de pares ordenados existe uma operação de projecção específica.
2.2.1 Variáveis ligadas
O nome das variáveis ligadas é irrelevante nos pré-termos e pré-tipos definidos pela gramática
da secção anterior. Assim os pré-termos e os pré-tipos devem ser considerados módulo renomeação das suas variáveis ligadas. Para obter este efeito, associamos uma regra de identidade
sintáctica a cada uma das cinco construções que introduzem variáveis ligadas:
ΛX:Κ.κ ≡ ΛY:Κ.(κ[Y/X])
µX:Κ.κ ≡ µY:Κ.(κ[Y/X])
∀ X≤ϕ[X].τ ≡ ∀ Y≤ϕ[Y].(τ[Y/X])
λX≤ϕ[X].e ≡ λY≤ϕ[Y].(e[Y/X])
λx:υ.e ≡ λy:υ.(e[y/x])
onde Y∉FV(κ)
onde Y∉FV(κ)
onde Y∉FV(ϕ) e Y∉FV(τ)
onde Y∉FV(ϕ) e Y∉FV(e)
onde y∉FV(e)
Nestas regras, A[Y/X] representa a substituição sem captura de X por Y em A [Bar84]. Uma outra técnica, que não usamos, de abstracção do nome das variáveis ligadas, consiste no uso de
índices de de Bruijn em vez de nomes alfabéticos [dB72].
Como é hábito nos sistemas de tipos, o sistema F+ usa contextos para registar as variáveis
ligadas introduzidas nos seus termos (cf. secção 2.3.1). Por construção, num contexto não podem coexistir duas variáveis ligadas com o mesmo nome. À partida, esta restrição torna a validade dos tipos e termos dependente dos nomes das variáveis ligadas: por exemplo, o termo
λx:υ.(λx:τ.x) não admite derivação formal directa no nosso sistema pois nele ocorrem duas variáveis ligadas homónimas com âmbitos não disjuntos. Mas as regras de identidade sintáctica,
atrás definidas, libertam a linguagem dessa dependência. De facto, o termo anterior é equivalente ao termo λx:υ.(λy:τ.y), o qual já admite derivação formal directa.
2.3 Sistema de tipos de F+
Apresentamos nesta extensa secção o sistema de tipos de F+. O sistema de tipos de F+ compreende regras de boa formação de contextos, regras de boa formação de tipos, regras de boa formação de termos, uma relação de identidade entre tipos e uma relação de subtipo. Na especificação de F+ empregamos as técnicas habituais de formalização de sistemas de tipos, apresentadas, por exemplo, em [Car97].
2 O sistema F+
17
2.3.1 Juízos
O sistema de tipos de F+ é um sistema de prova sobre juízos com cinco formas distintas, cada
uma delas com um significado distinto:
Γ
Γ
Γ
Γ
Γ
◊
τ:Κ
τ=τ′:Κ
τ≤τ′
e:τ
Γ é um contexto bem formado
o tipo τ pertence ao género Κ em Γ
os tipos τ e τ′ do género Κ, são idênticos em Γ
o tipo τ é subtipo de τ′ em Γ
o termo e tem tipo τ em Γ
Os juízo válidos do sistema são aqueles para os quais se podem construir, usando as regras
do sistema, uma árvore de prova cujas folhas são todas instâncias de axiomas. Um axioma é
uma regra sem premissas.
2.3.1.1 Contextos
A parte dum juízo que precede o símbolo chama-se contexto. Um contexto Γ é uma sequência finita ordenada de variáveis de valor com os respectivos tipos, x:τ , e variáveis de tipo com
os respectivos limites superiores, X≤ϕ[X]. Num contexto todas as variáveis têm de ser distintas.
Nas expressões de tipo podem ocorrer variáveis, mas só se estas já tiverem sido introduzidas
antes, no mesmo contexto. O contexto vazio denota-se por ∅. Eis um exemplo de contexto
bem formado:
∅,x:Nat,y:Nat→Nat,Z≤Point[Z],h:Z→Nat
O conjunto das variáveis que ocorrem num contexto Γ é representado por dom(Γ). O operador vírgula é usado para representar tanto a extensão dum contexto com uma nova variável –
Γ,x:τ ou Γ,X≤ϕ[X] – como a concatenação de dois contextos – Γ,Γ′. No primeiro caso requere-se
que a variável a adicionar não pertença ao domínio de Γ; no caso da concatenação requere-se
que os domínios dos dois contextos sejam disjuntos.
As regras que definem os contextos bem formados de F+ são as seguintes:
[Contexto vazio]
∅ ◊
[Contexto x]
Γ ◊ Γ τ:∗ x∉dom(Γ)
Γ‚x:τ ◊
[Contexto X≤κ]
Γ ◊ Γ κ:Κ X∉dom(Γ)
Γ‚X≤κ ◊
[Contexto X≤ϕ[X]]
Γ ◊ Γ ϕ:Κ⇒Κ X∉dom(Γ)
Γ‚X≤ϕ[X] ◊
[Contexto X]
Γ ◊ Γ Κ género X∉dom(Γ)
Γ‚X:Κ ◊
2.3.1.2 Asserções
A parte dum juízo que sucede o símbolo chama-se asserção. Todas as variáveis livres que
ocorrem numa asserção têm de estar declaradas no contexto respectivo.
Em situações em que o contexto Γ permanece fixo, é mais fácil trabalhar com simples asserções, deixando o contexto implícito, do que trabalhar com juízos completos. Esta é um prá-
18
OM – Uma linguagem de programação multiparadigma
tica habitual que também seguiremos. Assim, por exemplo, o termo asserção válida referir-se-á a um juízo válido no qual o contexto foi deixado implícito.
2.3.2 Sublinguagem dos tipos
A sublinguagem dos tipos de F+ inclui: uma forma de abstracção-lambda, uma operação de
aplicação, um construtor de pares ordenados de tipos e duas operações de projecção. A sublinguagem dos tipos revela-se assim, ela mesma, como um cálculo-lambda tipificado de primeira
ordem.
Apresentamos nesta secção o sistema de tipos da sublinguagem dos tipos ou seja o meta-sistema de tipos de F+. Os meta-tipos designam-se por géneros (kinds). Cada género representa um conjunto particular de pré-tipos bem formados ou, mais simplesmente, de tipos.
2.3.2.1 Apresentação das regras de boa formação dos tipos
A maioria das regras desta secção são auto-explicativas, pelo que só comentamos algumas delas.
A regra [Tipo X] mostra que o limite superior das variáveis F-restringidas é sempre baseado
num operador de tipo do género Κ⇒Κ (note bem: com domínio e contra-domínio do mesmo
género).
A regra [Tipo Top] indica que em cada género Κ existe um elemento designado por Top(Κ).
Mais adiante, quando definirmos a relação de supertipo, esse elemento será considerado supertipo de todos os elementos do seu género.
Na regra [Tipo µ], κ∠X é uma condição elementar que significa que em µX:Κ.κ o tipo κ é
contractivo ou não-trivial em X , isto é κ≡/ X e se κ≡µY:Κ.κ′ então κ′∠X (cf. [AC93, AF96]). Um
exemplo: de acordo com esta regra, o pré-tipo µX:∗.X não está bem formado, o que é consistente com a ideia de que a equação recursiva trivial X=X não tem solução canónica.
Na regra [Tipo {…}] é considerada a possibilidade dum tipo-registo não ter componentes.
Daí o requisito explicito do contexto Γ ser bem formado.
2.3.2.2 Regras de boa formação dos tipos
[Tipo X]
Γ‚X≤ϕ[X]‚Γ′ ϕ:Κ⇒Κ
Γ‚X≤ϕ[X]‚Γ′ X:Κ
[Tipo ×]
Γ κ1 :Κ1 Γ κ2 :Κ2
Γ <κ1 ‚κ 2 >:Κ 1 ×Κ2
[Tipo Top]
Γ ◊
Γ Top(Κ):Κ
[Tipo .]
Γ κ:Κ1 ×Κ2
Γ κ.n:Κn (n=1,2)
[Tipo Λ]
Γ‚X:Κ x κ:Κ
Γ
ΛX:Κx .κ:Κx ⇒Κ
[Tipo µ]
Γ‚X:Κ κ:Κ κ∠X
Γ µX:Κ.κ:Κ
[Tipo aplic Λ]
Γ ϕ:Κx ⇒Κ Γ κ:Κ
Γ ϕ[κ]:Κ
[Tipo ∀]
Γ‚X≤ϕ[X] τ:∗
Γ
∀ X≤ϕ[X].τ:∗
2 O sistema F+
19
[Tipo →]
Γ υ:∗ Γ τ:∗
Γ υ→τ:∗
[Tipo {…}]
– –
Γ ◊ Γ τ:∗ l distintos
––
Γ {l :τ}:∗
2.3.3 Identidade entre tipos
Apresentamos agora as regras que definem a relação de identidade ou igualdade entre os tipos
de F+: trata-se portanto da teoria equacional da sublinguagem dos tipos de F+.
2.3.3.1 Apresentação das regras de identidade entre tipos
As regras [Tipo= fold/unfold µ] e [Tipo= contracção µ] estabelecem que todo o tipo recursivo
µX:Κ.κ:Κ é idêntico à sua própria expansão infinita (cf. [AC93, AF96]). Para exemplificar o
uso destas duas regras, provamos agora que os dois tipos recursivos, κ1 =ˆ µY:Κ.Nat→Y e
κ2 =ˆ µZ:Κ.Nat→Nat→Z, são idênticos. Como não é possível transformar um dos tipos no outro
usando apenas a regra [Tipo= fold/unfold µ] , vamos explorar um outro caminho usando a regra
[Tipo= contracção µ]. Para isso temos de encontrar um tipo κ contractivo em X tal que κ 1 =κ[κ 1 /X] e
κ2 =κ[κ 2 /X]. No presente exemplo, esse tipo é fácil de encontrar: basta reduzir κ 1 e κ 2 à mesma
forma na sua parte finita através do uso repetido da regra [Tipo= fold/unfold µ] num contexto Γ
arbitrário:
κ1
= µY:Κ.Nat→Y
= Nat→(µY:Κ.Nat→Y)
= Nat→Nat→(µY:Κ.Nat→Y)
= Nat→Nat→κ1
κ2
= µY:Κ.Nat→Nat→Y
= Nat→Nat→(µY:Κ.Nat→Nat→Y)
= Nat→Nat→κ2
O tipo contractivo pretendido é κ =ˆ Nat→Nat→X. Agora, por aplicação de [Tipo= contracção µ], obtemos κ1 =κ2 , o que conclui a demonstração. Nem todas as provas são tão simples como esta:
ver, por exemplo, a demonstração de µX:Κ.X→(X→X)=µX:Κ.(X→X)→X em [AC93].
A relação de identidade, e também a relação de subtipo, são decidíveis no contexto do sistema monomórfico com tipos recursivos apresentado em [AC93]. Segundo Amadio e Cardelli
este resultado de decidibilidade generaliza-se a sistemas mais complexos, incluindo sistemas
de segunda ordem. Mais precisamente, podem adicionar-se tipos recursivos a qualquer sistema, que a característica de decidibilidade ou indecidibilidade do sistema original se mantém
no novo sistema.
Ainda relativamente à regra [Tipo= fold/unfold µ] , vale a pena referir que ela permite tipificar
a auto-aplicação “xx”. De facto, a operação de auto-aplicação está definida para todos os termos que tenham o tipo µX:∗.X→τ, onde τ é um tipo qualquer. Isto acontece porque, muito
simplesmente, Γ µX:∗.X→τ=(µX:∗.X→τ)→τ. Esta possibilidade de tipificar a auto-aplicação tem
20
OM – Uma linguagem de programação multiparadigma
como consequência a perda na propriedade de normalização forte: se >> representar uma relação de redução derivada das regras de F+ e se Rτ=µX:∗.X→τ, então, usando >> , o termo
divergeτ=(λx:R τ.xx)(λx:Rτ.xx) não converge para qualquer forma normal (apesar de, formalmente,
ter o tipo τ ). De qualquer forma, a confluência do sistema não fica comprometida.
A regra [Tipo= β Λ] define a operação de aplicação dum operador de tipo a um tipo-argumento.
A regra [Tipo= η Λ], serve para introduzir uma noção de identidade entre operadores de tipo
extensionalmente idênticos, ou seja, idênticos ponto a ponto ao longo de todo o seu domínio.
Efectivamente, conjugando esta regra com a seguinte outra regra [Tipo= Λ], prova-se que se
Γ‚X:Κ x ϕ[X]=ϕ′[X]:Κ então Γ ϕ=ϕ′:Κx ⇒Κ.
A regra [Tipo= proj ×] define as operações de projecção de pares ordenados de tipos e a regra
[Tipo= sp ×] é uma regra de extensionalidade adaptada ao caso dos pares ordenados (usamos sp
como abreviatura de surjective pairing, nome pelo qual a versão desta regra para pares de termos é conhecida em PCF).
A regra [Tipo= permutação {…}] indica que a ordem das etiquetas num tipo-registo é irrelevante.
As regras [Tipo= Top ⇒] e [Tipo= Top ×] identificam o tipo máximo dos géneros estruturados.
A regra [Tipo= género] indica que se um tipo está em relação consigo próprio para um dado
género, então esse tipo pertence a esse género.
As regras [Tipo= refl], [Tipo= sim] e [Tipo= trans] fazem de = uma relação de equivalência.
As oito regras finais tornam = numa congruência, isto é numa relação que permite a substituição de iguais por iguais: neste caso, a substituição de uma ou mais subcomponentes dum
tipo por componentes formalmente idênticas.
2.3.3.2 Regras de identidade entre tipos
[Tipo= fold/unfold µ]
Γ‚X:Κ κ:Κ κ∠X
Γ µX:Κ.κ=κ[(µY:Κ.κ)/X]:Κ
[Tipo= contracção µ]
Γ κ1 =κ[κ 1 /X]:Κ Γ κ2 =κ[κ 2 /X]:Κ κ∠X
Γ κ1 =κ2 :Κ
[Tipo= β Λ ]
Γ‚X:Κ x κ:Κ Γ κx :Κx
[Tipo= η Λ]
Γ ϕ:Κx ⇒Κ X∉dom(Γ)
Γ (Λ X:Κx .κ)[κx ]=κ[κx /X]:Κ
[Tipo= proj ×]
Γ κ1 :Κ1 Γ κ2 :Κ2
Γ <κ1 ‚κ 2 >.n=κn :Κn (n=1,2)
Γ
ΛX:Κx .ϕ[X]=ϕ:Κx ⇒Κ
[Tipo= sp ×]
Γ κ:Κ1 ×Κ2
Γ <κ.1‚κ.2>=κ:Κ 1 ×Κ2
[Tipo= permutação {…}]
– –
Γ ◊ Γ τ:∗ l distintos π permutação de {1‚…‚n}
––
Γ {l :τ}={lπ(1):τπ(1)‚…‚lπ(n):τπ(n)}:∗
2 O sistema F+
[Tipo= Top ⇒]
21
[Tipo= Top ×]
Γ ◊
Γ Top(Κ⇒Κ′)=ΛX:Κ.Top(Κ′):Κ⇒Κ′
[Tipo= género]
Γ κ=κ:Κ
Γ κ:Κ
[Tipo= refl]
Γ κ:Κ
Γ κ=κ:Κ
[Tipo= Λ ]
Γ‚X:Κ x κ=κ′:Κ
Γ
ΛX:Κx .κ= ΛX:Κx .κ′:Κx ⇒Κ
Γ ◊
Γ Top(Κ×Κ′)=<Top(Κ)‚Top(Κ′)>:Κ×Κ′
[Tipo= sim]
Γ κ=κ′:Κ
Γ κ′=κ:Κ
[Tipo= trans]
Γ κ=κ′:Κ Γ κ′=κ′′:Κ
Γ κ=κ′′:Κ
[Tipo= aplic Λ ]
Γ ϕ=ϕ′:Κx ⇒Κ Γ κ=κ′:Κ
Γ ϕ[κ]=ϕ′[κ′]:Κ
[Tipo= ×]
Γ κ1 =κ1 ′:Κ1 Γ κ2 =κ2 ′:Κ2
Γ <κ1 ‚κ 2 >=<κ1 ′‚κ2 ′>:Κ1 ×Κ2
[Tipo= .]
Γ κ=κ′:Κ1 ×Κ2
Γ κ.n=κ′.n:Κn (n=1,2)
[Tipo= µ]
Γ‚X:Κ κ=κ′:Κ κ∠X κ′∠X
Γ µX:Κ.κ=µX:Κ.κ′:Κ
[Tipo= ∀ ]
Γ‚X≤ϕ[X] τ=τ′:∗
[Tipo= →]
Γ υ=υ′:∗ Γ τ=τ′:∗
Γ υ→τ=υ′→τ′:∗
[Tipo= {…}]
– –
–
Γ ◊ Γ τ=τ′:∗ l distintos
–– – –
Γ {l :τ}={l :τ′}:∗
Γ
∀ X≤ϕ[X].τ= ∀X≤ϕ[X].τ′:∗
2.3.4 Subtipos
O tipo τ é subtipo do tipo τ′, e escreve-se τ≤τ′, se toda a expressão do tipo τ puder ser usada nos
contextos sintácticos onde se espera uma expressão do tipo τ′ (cf. regra [Termo inclusão] da secção 2.3.5). Interpretando um tipo como um conjunto de termos, a relação de subtipo corresponde à relação de inclusão entre conjuntos de termos.
A noção de subtipo, permite que uma função de domínio τ′ possa ser aplicada a termos de
vários tipos τ bastando para isso que τ≤τ′. Esta é uma forma particular de polimorfismo denominada de polimorfismo de inclusão e identificada pela primeira vez em [CW85].
As regras desta secção axiomatizam a relação de subtipo no sistema F+. Esta é uma ordem
parcial, conforme o indicam as regras [Sub refl] , [Sub anti-sim] e [Sub trans]. Note que a introdução
duma regra de anti-simetria é, implicitamente, uma manifestação da nossa intenção de interpretar a relação de subtipo como a relação de inclusão entre conjuntos de termos. Eliminando
esta regra seria possível a interpretação, mais geral, da relação de subtipo como relação de
convertibilidade entre tipos.
2.3.4.1 Apresentação das regras de subtipo
A regra [Sub =] estabelece a conexão entre os juízos da relação identidade entre tipos e os juízos da relação de subtipo: se dois tipos são idênticos, então um deles é subtipo do outro.
A regra [Sub X] permite a utilização de variáveis definidas no contexto.
22
OM – Uma linguagem de programação multiparadigma
A regra [Sub Top] indica que o tipo Top(Κ), anteriormente introduzido na regra [Tipo Top], é
supertipo de todos os tipos do género Κ.
[Sub Λ ] define
a relação de subtipo entre operadores de tipo, ponto a ponto.
A regra [Sub →] estabelece que a relação de subtipo entre tipos funcionais requer contravariância (ou antimonotonia) no tipo do argumento e covariância (ou monotonia) no tipo do resultado (cf. [Car97]). A justificação intuitiva desta regra pode fazer-se considerando a seguinte
questão: se tivermos f:υ→τ e f′:υ′→τ′, em que condições é possível usar f num contexto que espera uma função com o tipo de f′ ? Em primeiro lugar se f′ pode ser aplicada a qualquer valor
do tipo υ′ também f o deve poder ser, de onde se obtém o primeiro requisito: υ′≤υ. Em segundo
lugar, se os resultados de f′ podem ser usado em contextos que esperam termos do tipo τ′, então os resultados produzidos por f também devem aí poder ser usados, de onde se obtém o
segundo requisito: τ≤τ′.
Segundo a regra [Sub µ] , o tipo µX:Κ.κ é subtipo de µY:Κ.κ′ se a partir da assunção X≤Y for
possível provar κ≤κ′. Por exemplo, se é verdade que µX:Κ.Nat→X≤µY:Κ.Nat→Y, já não é verdade
que µX:Κ.X→Nat≤µY:Κ.Y→Nat. Um detalhe interessante é o facto desta regra não ser suficiente
para provar que µX:Κ.(X→Nat) é subtipo de si próprio: é necessário usar a regra [Sub refl] para
deduzir este facto.
Introduzimos a regra [Sub κµ], não definida em [AC93], pois teremos necessidade dela no
próximo capítulo. A regra estabelece que se κ≤ϕ[κ] e se ϕ for um operador de tipo crescente
então κ≤µZ:Κ.ϕ[Z]. Note que das premissas se obtém a seguinte cadeia infinita crescente
κ≤ϕ[κ]≤ϕ[ϕ[κ]]≤…., que conduz à condição κ≤ϕn [κ], para todo o n>0 , o que mostra que a asserção
κ≤µZ:Κ.ϕ[Z] faz sentido. Convém ter em mente que a relação de subtipo é formalizada em
[AC93] usando uma noção de ordem entre árvores infinitas, definida à custa de aproximações
finitas.
A regra [Sub ∀ ] estabelece que a relação de subtipo entre tipos universais F-restringidos requer uma forma de contravariância generalizada da restrição sobre o argumento e covariância
normal no tipo do resultado. Podemos obter uma justificação intuitiva para esta regra raciocinando como na regra [Sub →]. Se tivermos P: ∀X≤ϕ[X].τ e P′: ∀X≤ϕ′[X].τ′, em que condições será
possível usar P num contexto que espera uma função polimórfica com o tipo de P′? Em primeiro lugar, se P′ pode ser aplicada a um X qualquer tal que X≤ϕ′[X], então P também deve poder
ser aplicada a esse mesmo X, o que só é possível se também se verificar X≤ϕ[X]: isto justifica a
condição, que chamamos de contravariância generalizada, Γ‚X≤ϕ′[X] X≤ϕ[X]. Em segundo lugar, se os resultados de P′ podem ser usados em contextos onde se esperam termos do tipo τ′
(dependente de X≤ϕ′[X]), então os resultados produzidos por P também devem poder ser aí usados, de onde se obtém a segunda exigência: Γ‚X≤ϕ′[X] τ≤τ′. As duas condições que obtivemos
são as principais premissas da regra [Sub ∀]. Esta regra generaliza a regra correspondente apresentada em [CG92, Car97] para tipos universais restringidos, a qual também apresenta uma
forma de contravariância nos limites dos argumentos.
2 O sistema F+
23
Finalmente, a regra [Sub {…}] estabelece que se para os dois tipos-registo ρ e ρ′ tivermos
ρ≤ρ′, então ρ inclui necessariamente as etiquetas de ρ′ e os tipos das componentes de ρ′ são supertipos dos tipos das componentes respectivas de ρ . Se cada registo for interpretado como
uma função com um domínio de etiquetas, esta regra tem uma analogia com [Sub →]: a contravariância no domínio dos registos e a covariância nos tipos das componentes homónimas.
2.3.4.2 Regras de subtipo
[Sub refl]
Γ κ:Κ
Γ κ≤κ
[Sub anti-sim]
Γ κ≤κ′ Γ κ′≤κ
Γ κ=κ′
[Sub X]
Γ‚X≤ϕ[X]‚Γ′ ◊
Γ‚X≤ϕ[X]‚Γ′ X≤ϕ[X]
[Sub Top]
Γ κ:Κ
Γ κ≤Top(Κ)
[Sub µ]
Γ‚Y:Κ‚X≤Y κ≤κ′ κ∠X κ′∠Y
Γ µX:Κ.κ≤µY:Κ.κ′
[Sub ×]
Γ κ1 ≤κ 1 ′ Γ κ2 ≤κ 2 ′
Γ <κ1 ‚κ 2 >≤<κ1 ′‚κ2 ′>
[Sub Λ ]
Γ‚X:Κ κ≤κ′
Γ
ΛX:Κ.κ≤ ΛX:Κ.κ′
∀ X≤ϕ[X].τ≤ ∀X≤ϕ′[X].τ′
[Sub =]
Γ κ=κ′:Κ
Γ κ≤κ′
[Sub aplic Λ ]
Γ ϕ≤ϕ′ Γ ϕ[κ]:Κ
Γ ϕ[κ]≤ϕ′[κ]
[Sub κµ]
Γ κ≤ϕ[κ] Γ‚Y:Κ‚X≤Y ϕ[X]≤ϕ[Y] ϕ[Z]∠Z
Γ κ≤µZ:Κ.ϕ[Z]
[Sub .]
Γ κ≤κ′ Γ κ′:Κ1 ×Κ2
Γ κ.n≤κ′.n (n=1,2)
[Sub ∀ ]
Γ‚X≤ϕ′[X] X≤ϕ[X] Γ‚X≤ϕ′[X] τ≤τ′
Γ ∀ X≤ϕ[X].τ:∗
Γ
[Sub trans]
Γ κ≤κ′ Γ κ′≤κ′′
Γ κ≤κ′′
[Sub →]
Γ υ′≤υ Γ τ≤τ′ Γ υ→τ:∗
Γ υ→τ≤υ′→τ′
[Sub {…}]
Γ τ1 ≤τ 1 ′ … Γ τk ≤τ k ′
Γ {l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }:∗
Γ {l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }≤{l 1 :τ1 ′‚…‚l k :τk ′}
2.3.4.3 Noção de polaridade
A contravariância (ou antimonotonia) do argumento dos tipos funcionais que é estabelecida
pela regra [Sub →] é fonte de diversas complicações: a regra é contra-intuitiva, torna a relação
de subtipo algo tortuosa, e impede a construção de modelos semânticos usando as técnicas de
aproximação habituais (o construtor → não é monótono logo, por maioria de razão, não é contínuo [BM92]).
Para lidar com os problemas criados por →, é útil dispor da noção de polaridade da ocorrência duma subexpressão de tipo dentro duma expressão de tipo mais ampla.
Definição 2.3.4.3-1 (Polaridade) Seja σ uma subexpressão do tipo τ e seja σ′ um subtipo
genérico de σ. Seja τ′ o tipo que se obtém a partir de τ substituindo uma ocorrência particular
de σ pelo seu subtipo estrito σ′ . Então essa ocorrência de σ diz-se positiva se se verificar τ′≤τ,
ou seja, se os efeitos da alteração forem covariantes com a substituição. Diz-se negativa se
τ≤τ′, ou seja, se os efeitos da alteração forem contravariantes com a substituição. No primeiro
caso também se diz que σ ocorre positivamente em τ , e no segundo caso que σ ocorre negativamente em τ.
24
OM – Uma linguagem de programação multiparadigma
Vejamos alguns exemplos:
Na expressão de tipo υ→τ, a polaridade da ocorrência visível de υ é negativa, e a polaridade
da ocorrência visível de τ é positiva.
Na expressão de tipo ( υ→τ)→σ, υ e σ têm polaridade positiva, e υ→τ e τ têm polaridade negativa. Note que o construtor → inverte a polaridade das ocorrências das subexpressões do domínio e preserva a polaridade das ocorrências das subexpressões do contradomínio.
Na expressão de tipo µX:Κ.X→τ , a ocorrência de τ não tem polaridade, como se pode concluir da aplicação da regra [Sub µ]. Note que µX:Κ.X→τ=(((…)→τ)→τ)→τ . Já na expressão de tipo
µX:Κ.τ→X, a ocorrência de τ tem polaridade negativa.
De forma geral, os construtores de tipo preservam a polaridade das subexpressões. As excepções são os tipos funcionais (ocorrências no domínio), os tipos recursivos (ocorrências em
qualquer sítio quando a variável de recursão ocorre negativamente), e ainda os tipos universais
F-restringidos (ocorrências no limite superior).
2.3.4.4 Indecidibilidade da relação de subtipo em F≤
A relação de subtipo é indecidível em F≤, ou seja, não existe qualquer algoritmo que permita
verificar se um tipo é subtipo de outro em F≤ (cf. [Pie93, Ghe93, CP94]). Para verificar a relação de subtipo em F≤ existem apenas semialgoritmos os quais, por definição, podem não terminar em alguns casos.
Pierce provou a indecidibilidade da relação de subtipo em F≤ mostrando que qualquer expressão lógica envolvendo a relação de subtipo de F≤ era redutível a uma máquina de dois contadores (no sentido de [HU79]), para as quais o halting problem é indecidível.
Na prática, o problema de indecidibilidade pode não ser excessivamente grave. Em primeiro lugar, um sistema indecidível continua a ser útil para definir a semântica de linguagens com
sistemas de tipos que possam ser demonstrados decidíveis de forma independente. Em segundo lugar, como afirma Pierce [Pie93], os tipos que provocam a não terminação do algoritmo
semidecidível de tipificação para F≤ [CG92] são demasiado complicados e artificiais para
surgirem por acidente numa situação real.
Finalmente, convém dizer que o problema da indecidibilidade de F≤ pode ser eliminado
através da introdução de restrições sintácticas artificiais. Por exemplo, adoptando a versão da
regra [Sub ∀ ] originalmente introduzida em [CW85]: esta regra impõe que duas abstracções da
forma ∀ X≤υ.τ tenham de ter o mesmo limite superior para que se possam candidatar a pertencer à relação de subtipo (cf. [CP94, SP94]).
2 O sistema F+
25
2.3.4.5 F+d subsistema decidível de F+
Vimos no ponto anterior que a relação de subtipo era indecidível no sistema F≤. Ora a indecidibilidade de F≤ propaga-se a F+: em F+ continua a ser possível usar o procedimento de redução de Pierce, não obstante, relativamente a F≤, a regra [Sub ∀] ter sido um pouco alterada e terem sido introduzidas novas regras de subtipo para as novas formas de tipos.
Nesta secção criaremos uma variante decidível de F+ a que chamaremos F+d. Existem diversas formas de restringir F+ para alcançar esse objectivo (já referimos uma no final da secção
anterior). Vamos escolher a forma que melhor serve a conveniência da linguagem OM. Como
sabemos, a semântica desta linguagem será definida por tradução para F+, ou mais exactamente F+d, ao longo dos próximos capítulos.
Levando em conta a conveniência da linguagem OM, a variante decidível de F+ deve ser
obtida por eliminação da regra de transitividade [Sub trans]. Por isso definimos F+d da seguinte
forma:
+
Definição 2.3.4.5-1 (Sistema Fd ) O sistema F+d é o subsistema de F+ que se obtém a
partir deste removendo a regra da transitividade [Sub trans].
Para começar, vejamos quais as consequências da eliminação de [Sub trans]. Na referência
[CG92], Currien e Ghelli mostraram que em F≤, a regra de transitividade é de uso redundante
na maioria das situações, podendo ser substituída pela seguinte regra, bastante mais simples:
[Sub var-trans]
Γ‚X≤υ υ≤τ
Γ‚X≤υ X≤τ
Esta regra define uma forma fraca de transitividade que se caracteriza por envolver sempre
uma variável de tipo definida no contexto corrente. A substituição da regra [Sub trans] também é
possível em F+, mas neste caso tem de ser trocada por uma variante um pouco mais complicada, pois X pode representar um operador de tipo variável (alguma da motivação para esta regra pode ser encontrada em [SP94]):
[Sub var-trans2]
Γ‚X≤ϕ[X] (ϕ[X][υ1 ]…[υn ])≤τ Γ‚X≤ϕ[X] (ϕ[X][υ1 ]…[υn ]):Κ
Γ‚X≤ϕ[X] X[υ1 ]…[υn ]≤τ
De qualquer forma não pretendemos manter no sistema F+d qualquer regra de transitividade,
mesmo numa variante fraca. Mas esta discussão foi útil para percebermos que da remoção da
regra [Sub trans] não resulta qualquer perda de transitividade na relação ≤ no domínio dos tipos
concretos, isto é no domínio dos tipos que não são variáveis de tipo. Apenas as variáveis de
tipo ficam prejudicadas pela eliminação da regra de transitividade. Com efeito, agora, sem
regra [Sub trans], do contexto Γ‚X≤ϕ[X]‚Γ′ só se podem deduzir as duas asserções triviais X≤X
(usando a regra [Sub refl] ), e X≤ϕ[X] (usando a regra [Sub X]).
26
OM – Uma linguagem de programação multiparadigma
Vejamos agora por que razão a remoção da regra [Sub trans] se adequa às particularidades da
linguagem OM. A linguagem OM usa construções de segunda ordem essencialmente em duas
situações: na formalização do conceito de classe (cf. capítulo 5) e na definição duma forma específica de polimorfismo paramétrico designada por polimorfismo paramétrico ≤ * -restringido
(cf. capítulo 6). Em qualquer dos casos, uma variável de tipo X é sempre introduzida sob uma
restrição da forma X≤ϕ[X], onde ϕ representa uma interface de classe. A restrição X≤ϕ[X] lê-se
assim: “a variável de tipo X representa um tipo-objecto indeterminado compatível com a interface ϕ”. Portanto, enquanto X representa um tipo-objecto, ϕ[X] não representa um tipo-objecto
mas sim a funcionalidade mínima de X . Assim interessa evitar que X e ϕ[X] possam ser misturados numa aplicação duma hipotética regra de transitividade, como se se tratassem de entidades da mesma natureza. Ou seja: da restrição X≤ϕ[X], gostaríamos de poder deduzir apenas as
asserções triviais: X≤X (porque X é um tipo), e X≤ϕ[X] (porque precisamos de saber qual é a
funcionalidade mínima de X ). A remoção da regra da transitividade permite alcançar este
objectivo.
Falta provar que F+d é decidível.
+
Teorema 2.3.4.5-2 (Decidibilidade da relação de subtipo em Fd ) A relação de
subtipo no sistema F+d é decidível.
Prova: (Esboço) Temos recorrer a alguns resultados do futuro capítulo 10. Em primeiro lugar,
por inspecção verificamos que todas as regras de subtipo não terminais de F+d (cf. definição
10.2.5-1) obedecem à propriedade da subfórmula (cf. definição 10.2.7.3.2-1). As regras [Sub µ]
e [Sub κµ] são um caso à parte, mas serão tratáveis, à luz dos comentário sobre “tipos recursivos e decidibilidade” da secção 2.3.3.1.
Consideremos agora o procedimento de prova indeterminista completo descrito na definição 10.2.7.3.1-1. Pelo teorema 10.2.7.3.2-2, esse procedimento é um algoritmo (indeterminista) completo. Portanto a relação de subtipo de F +d é decidível.
+
Teorema 2.3.4.5-3 (Decidibilidade de F d ) O sistema F+d é decidível.
Prova: (Esboço) Para desenvolver um algoritmo de tipificação para F+d, ou mais exactamente
de tipificação mínima, elimina-se a regra [Termo inclusão] (esta é uma regra problemática) e
generalizam- as regras de aplicação [Termo aplic →], [Termo aplic ∀] e [Tipo aplic Λ] para compensar a eliminação da regra anterior. Depois prova-se que o novo sistema de regras define um algoritmo determinista para atribuição de tipos mínimos a termos (cf. [CG92, SF94]).
Em [CG92] o procedimento que acabámos de descrever é apresentado em todos os seus detalhes para o caso do sistema F≤. Em [SP94], adapta-se esse procedimento ao caso do sistema
Fω
≤ . Em ambos os casos, estão em causa exercícios não excessivamente complicados, sendo o
segundo caso um pouco mais mais rico por estarem envolvidos operadores de tipos e géneros.
2 O sistema F+
27
Quanto a F+d, o seu tratamento é quase idêntico ao tratamento de F ω
≤ , pois as únicas novas
formas de termos a considerar são os registos e as abstracções paramétricas F-restringidas.
Para finalizar referimos brevemente os trabalhos [BCD+93] e [BCK94], nos quais se discute e prova a decidibilidade da relação de subtipo numa linguagem orientada pelos objectos
chamada TOOPLE. Na definição da semântica da linguagem usa-se polimorfismo paramétrico
F-restringido, sendo esse um aspecto em comum com a nossa linguagem OM.
No entanto a linguagem TOOPLE é definida usando os métodos operacional e denotacional
– não por tradução para um cálculo-lambda – sendo o seu sistema de tipos especificado directamente usando um sistema de Post. Consequentemente, os algoritmos de tipificação e subtipificação são definidos directamente sobre os tipos da linguagem, incluindo complexos tipos-objecto. Para que o sistema fique decidível introduz-se a seguinte regra: “não é permitido
introduzir uma variável de tipo que seja subtipo dum tipo-objecto”. Curiosamente, esta acaba
por ser uma forma indirecta, e diferente da nossa, de desactivar a regra da transitividade para
subtipos de TOOPLE para o caso particular das variáveis de tipo.
2.3.4.6 Usos distintos de F+ e de F+d
Como afirmámos na secção anterior, a linguagem OM, que iremos introduzir ao longo dos
próximos capítulos, será definida por tradução para o sistema F +d.
No entanto, os teoremas sobre a linguagem serão todos demonstrados no contexto do sistema F+. A razão é a seguinte: a regra [Sub trans] é necessária em certas demonstrações, e só no
contexto de F+ esta regra se encontra disponível.
Mesmo que o possa parecer, esta nossa metodologia não é contraditória. Ela significa que o
modelo da linguagem OM é efectivamente definido sobre o sistema F +. Simplesmente, restringe-se um pouco a forma como F + pode ser usado na codificação das construções de OM para
efeitos de verificação mecânica da boa tipificação dos programas escritos em OM. O sistema
F+d captura essas restrições.
De agora em diante, será muito raro referirmo-nos directamente ao sistema F+d. Usaremos o
nome F+ para designar ambos os sistemas, ficando sempre subentendido que as equações
semânticas de OM estarão escritas em F +d.
2.3.5 Termos
Apresentamos agora as regras que caracterizam os pré-termos bem formados da linguagem ou,
mais simplesmente, os termos da linguagem.
28
OM – Uma linguagem de programação multiparadigma
2.3.5.1 Apresentação das regras de boa formação dos termos
A regra [Termo inclusão] estabelece a conexão entre os juízos da relação de subtipo e os juízos
relativos à boa formação dos termos. Esta regra introduz polimorfismo de inclusão no sistema.
A regra [Termo aplic ∀ ] mostra que quando se aplica uma função polimórfica a um tipo τ,
esse tipo influencia tanto o resultado da função como o tipo desse resultado.
A regra [Termo selecção {…}] permite lidar directamente com registos com uma única componente e indirectamente (usando as regras [Termo inclusão] e [Sub {…}] ) com registos com múltiplas componentes.
As restantes regras são auto-explicativas, pelo que não merecem comentários.
2.3.5.2 Regras de boa formação dos termos
[Termo inclusão]
Γ x:τ Γ τ≤τ′
Γ x:τ′
[Termo x]
Γ‚x:τ‚Γ′ ◊
Γ‚x:τ‚Γ′ x:τ
[Termo ∀ ]
Γ‚X≤ϕ[X] e:τ
[Termo aplic →]
Γ f:υ→τ Γ e:υ
Γ (f e):τ
[Termo aplic ∀ ]
Γ λX≤ϕ[X].e: ∀X≤ϕ[X].τ
[Termo {…}]
–– –
Γ ◊ Γ e:τ l distintos
– – ––
Γ {l =e}:{l :τ}
[Termo →]
Γ‚x:υ e:τ
Γ λx:υ.e:υ→τ
Γ P:∀ X≤ϕ[X].τ Γ υ≤ϕ[υ]
Γ P[υ]:τ[υ/X]
[Termo selecção {…}]
Γ e:{l:τ}
Γ e.l:τ
2.4 Teoria equacional para F+
Propomos agora uma teoria equacional para a linguagem dos termos de F+, concretizando
assim uma semântica para F+. Usamos, como é habitual (cf. [Gun92, Win93]), um sistema de
prova sobre juízos da forma:
Γ e=e′:τ
os termos e e e′ do tipo τ, são idênticos em Γ
2.4.1 Apresentação das regras de identidade entre termos
As regras [Termo= β λx], [Termo= β λX], [Termo= selecção {…}] definem duas operações de aplicação e a operação de selecção de componente de registo.
As regras [Termo= η λx], [Termo= η λX], [Termo= ext {…}] são regras de extensionalidade análogas à regra [Tipo= η Λ] .
A regra [Termo= permutação {…}] indica que a ordem das etiquetas num registo é irrelevante.
Este regra pode ser deduzida de [Tipo= permutação {…}] e [Termo= ext {…}].
2 O sistema F+
29
A regra [Termo= inclusão] é a adaptação da regra [Termo inclusão] para a relação de identidade
entre termos.
A regra [Termo= top-colapso] é uma regra característica do sistema F≤ que estabelece que dois
termos quaisquer se tornam idênticos quando vistos como elementos do tipo Top(∗). Note que,
em geral, dois termos podem ser considerados idênticos ou distintos, dependendo do tipo em
que são considerados. Por exemplo, {a=1, b=2, c=3} e {a=1, b=2} são idênticos no tipo
{a:Nat, b:Nat}, mas não são idênticos no tipo {a:Nat, b:Nat, c:Nat}.
A regra [Termo= tipo] permite deduzir que todos os termos que estão em relação consigo próprios num dado tipo são desse mesmo tipo.
As regras [Termo= refl], [Termo= sim] e [Termo= trans] fazem de = uma relação de equivalência.
As seis regras finais destinam-se apenas a tornar a relação = numa congruência, isto é numa
relação que permite a substituição de iguais por iguais, neste caso a substituição de subtermos
por termos formalmente idênticos.
2.4.2 Regras de identidade entre termos
[Termo= β λx]
Γ‚x:υ e:τ Γ a:υ
Γ (λx:υ.e)a=e[a/x]:τ
[Termo= η λx]
Γ e:υ→τ x∉dom(Γ)
Γ λx:υ.(e x)=e:υ→τ
[Termo= β λX]
Γ‚X≤ϕ[X] e:τ Γ υ≤ϕ[υ]
Γ (λX≤ϕ[X].e)[υ]=e[υ/X]:υ[υ/X]
[Termo= η λX]
Γ e:∀ X≤ϕ[X].τ X∉dom(Γ)
[Termo= selecção {…}]
––
Γ e:τ 1≤i≤n
– –
Γ {l =e}.li =ei :τi
[Termo= ext {…}]
––
Γ e:τ
– ––
––
Γ {l =(e.l )}=e:{l :τ}
Γ λX≤ϕ[X].e[X]=e: ∀X≤ϕ[X].τ
[Termo= permutação {…}]
–– –
Γ ◊ Γ e:τ l distintos π permutação de {1‚…‚n}
– –
––
Γ {l =e}={lπ(1)=eπ(1)‚…‚lπ(n)=eπ(n)}:{ l :τ}
[Termo= inclusão]
Γ x=x′:τ Γ τ≤τ′
Γ x=x′:τ′
[Termo= tipo]
Γ x=x:τ
Γ x:τ
[Termo= top-colapso]
Γ e:Top(∗) e′:Top(∗)
Γ e=e′:Top(∗)
[Termo= refl]
Γ x:τ
Γ x=x:τ
[Termo= sim]
Γ x=x′:τ
Γ x′=x:τ
[Termo= trans]
Γ x=x′:τ Γ x=x′′:τ
Γ x=x′′:τ
[Termo= λx]
Γ‚x:υ e=e′:τ
Γ λx:υ.e=λx:υ.e′:υ→τ
[Termo= aplic λx]
Γ f=f′:υ→τ Γ e=e′:υ
Γ (f e)=(f′ e′):τ
[Termo= λX]
Γ‚X≤ϕ[X] e=e′:τ
[Termo= aplic λX]
Γ P=P′:∀ X≤ϕ[X].τ Γ υ≤ϕ[υ]
Γ P[υ]=P′[υ]:τ[υ/X]
Γ λX≤ϕ[X].e=λX≤ϕ[X].e′:∀ X≤ϕ[X].τ
30
OM – Uma linguagem de programação multiparadigma
[Termo= {…}]
– – – –
Γ e=e′ :τ l distintos
– – – – ––
Γ {l =e}={l =e′ }:{l :τ}
[Termo= selecção {…}]
––
Γ e=e′:{l :τ} 1≤i≤n
Γ e.li =e′.li :τi
2.5 Formas derivadas
Nesta secção, introduzimos um extenso rol de novas formas de termos e tipos que teremos necessidade de usar nos próximos capítulos e que podem ser codificadas dentro do sistema F +
usando as suas construções primitivas. Esses novos termos e tipos serão chamados de formas
derivadas. Note que cada forma derivada constitui apenas sintaxe de alto nível para um padrão
complexo de elementos primitivos que seria inconveniente usar directamente. Se é verdade
que, semanticamente, as formas derivadas não introduzem nada de novo, já ao nível dos conceitos, as formas derivadas podem introduzir conceitos novos: por exemplo, os conceitos de
produto cartesiano de tipos, de par ordenado, de projecção, de valor recursivo, etc.
2.5.1 Pares ordenados
A codificação padrão de pares ordenados no sistema F, também válida em F+, é a seguinte
[BB85]:
τ×τ′
<e,e′>
e.1
e.2
=ˆ
=ˆ
=ˆ
=ˆ
∀ X:∗.(τ→τ′→X)→X
λX:∗.λf:τ→τ′→X.(f e e′)
e[τ](λx:τ.λy:τ′.x)
e[τ′](λx:τ.λy:τ′.y)
Esta codificação respeita a relação de subtipo, o que se deve ao facto de na definição de τ×τ′
cada um dos tipos componentes ocorrer duas vezes à esquerda de → o que os torna covariantes. Portanto a seguinte regra pode ser deduzida da codificação efectuada:
[Sub par]
Γ τ1 ≤τ 1 ′ Γ τ2 ≤τ 2 ′
Γ τ1 ×τ2 ≤τ 1 ′×τ2 ′
Note que a codificação de pares usando simples registos não reproduziria com exactidão a
relação de subtipo pretendida: só para dar um exemplo, teríamos sempre Γ τ1 ×τ2 ≤τ 1 . Já agora,
também a tentativa, inversa, de codificar registos usando tuplos daria problemas.
Tipos tuplos, tuplos de valores e tuplos de tipos são redutíveis a pares ordenados usando as
seguintes definições indutivas, onde n>2 :
τ1 ×τ2 ×…×τn =ˆ τ1 ×(τ2 ×…×τn )
<x 1 ,x 2 ,…,xn > =ˆ <x 1 ,<x2 ,…,xn >>
<τ1 ,τ2 ,…,xn > =ˆ <τ1 ,<τ 2 ,…,τn >>
2 O sistema F+
31
2.5.2 Operador de ponto fixo e valores recursivos
Na secção 2.3.3.1 vimos que os tipos recursivos permitiam tipificar a auto-aplicação. Desta
forma é possível, e simples, associar um operador de ponto fixo a cada tipo τ: basta adoptar o
operador paradoxal do cálculo-lambda não-tipificado às nossas circunstâncias [Bar84, Sto77]:
fixτ:(τ→τ)→τ
=ˆ λf:τ→τ.(λx:(µX:∗.X→τ).f (xx))(λx:(µX:∗.X→τ).f (xx))
O tipo deste operador fixτ é (τ→τ)→τ.
Verifiquemos agora que se trata, realmente, dum operador de ponto fixo. Seja f:τ→τ. Então:
fixτ f = (λx:(µX:∗.X→τ).f (xx))(λx:(µX:∗.X→τ).f (xx))
= f((λx:(µX:∗.X→τ).f (xx))(λx:(µX:∗.X→τ).f (xx)))
= f(fixτ f)
Vamos agora introduzir valores recursivos usando a seguinte definição:
recτ x:τ.e
=ˆ fix τ (λx:τ.e)
Verifiquemos a propriedade de unfolding do operador recτ:
recτ x:τ.e
= fixτ (λx:τ.e)
= (λx:τ.e)(fixτ (λx:τ.e))
= e[fixτ (λx:τ.e/x)]
= e[recτ x:τ.e/x]
2.5.3 Declarações locais de tipos e valores
Introduzimos agora a possibilidade de nomeação de tipos através de quatro formas distintas de
definição local.
Declaração de tipo não recursivo:
LET X:K=κ IN e
=ˆ e[κ/X]
(note que e[κ/X] = ( ΛX.e) κ)
Declaração de tuplo de tipos não recursivo:
LET <X 1 ,…, Xn >:Κ 1 ×…×Κn =κ IN e
=ˆ eσ[κ/X]
onde σ
=ˆ [X.1/X 1 , …, X.n/Xn ]
Declaração de tipo recursivo:
LET REC X:Κ=κ IN e
=ˆ e[µX:Κ.κ/X]
Declaração de tipos mutuamente recursivos:
LET REC <X 1 ,…, Xn >:Κ 1 ×…×Κn =κ IN e
=ˆ eσ[µX:Κ1 ×…×Κn .κσ/X]
onde σ =ˆ [X.1/X 1 , …, X.n/Xn ]
Analogamente, vamos também permitir a nomeação de valores por meio de quatro formas
distintas de definição local:
Declaração de valor não recursivo:
32
OM – Uma linguagem de programação multiparadigma
let x:υ=e in e′
=ˆ e′[e/x]
(note que e′[e/x] = (λx.e′) e)
Declaração de tuplo não recursivo:
let <x1 ,…, x n >:υ1 ×…×υ n =e in e′
=ˆ e′σ[e/x]
onde σ
=ˆ [x.1/x1 , …, x.n/xn ]
Declaração de valor recursivo:
let rec x:υ=e in e′
=ˆ e′[rec x:υ.e/x]
Declaração de tuplo recursivo:
let rec <x1 ,…, x n >:υ1 ×…×υ n =e in e′
=ˆ e′σ[rec x:υ1 ×…×υ n .eσ/x]
onde σ =ˆ [x.1/x1 , …, x.n/xn ]
2.5.4 Tipos existenciais
Deve-se a Mitchell e Plotkin a ideia de modelizar tipos abstractos [Mor73, LSA77, Rey78,
Rey83] usando tipos existenciais [MP85, CW85].
2.5.4.1 Exemplo de tipo existencial
Antes de definirmos uma codificação para os tipos existenciais em F+, convém introduzi-los
através dum exemplo de implementação e utilização dum tipo abstracto representado por um
tipo existencial. Vamos definir um tipo abstracto contador, identificado pelo nome Cont e possuindo as seguintes operações públicas: zero (contador zero), inc (incremento de contador) e eq
(comparação de dois contadores). Para representar internamente os contadores usaremos números inteiros. A representação interna dos contadores ficará oculta dos clientes do tipo abstracto os quais só terão acesso ao seu nome e, ainda, aos nomes e tipos das operações associadas.
O tipo existencial que captura a informação pública associada ao tipo abstracto Cont escreve-se da seguinte forma:
∃ Cont:∗.{zero:Cont, inc:Cont→Cont, eq:Cont→Cont→Bool}
Esta expressão lê-se informalmente da seguinte forma: existe um tipo abstracto, denominado Cont, cuja natureza exacta se desconhece, mas que suporta uma constante zero com tipo Cont ,
uma operação inc com tipo Cont→Cont e uma operação eq com tipo Cont→Cont→Bool.
Exibimos seguidamente uma implementação particular do tipo abstracto que usa o tipo-representação Nat e ainda uma utilização, por um termo cliente, desse tipo abstracto:
let P:∃Cont.{zero:Cont, inc:Cont→Cont, eq:Cont→Cont→Bool}
=ˆ pack Cont=Nat with {zero=0, inc=λc:Cont.c+1, eq=λc1:Cont.λc2:Cont.c1=c2} in
open P as Cont,ContI in
ContI.eq ContI.zero (ContI.inc ContI.zero)
2 O sistema F+
33
O termo da segunda linha, definido usando a construção pack, tem tipo existencial e concretiza
uma implementação do tipo abstracto pretendido. Um termo com tipo existencial designa-se
genericamente por pacote ou módulo. Na expressão acima, associamos o nome local P ao
pacote criado. A implementação tem acesso à representação interna do tipo. Na implementação, o nome Cont é considerado um simples nome alternativo para Nat.
Na terceira linha da expressão procedemos à abertura do pacote P para obtermos acesso local ao nome Cont e às operações públicas sobre contadores (note que, no entanto, a representação interna dos contadores permanece oculta para os termos-cliente). No contexto local introduzido pela construção open, isto é na quarta linha, introduzimos um termo cliente, o qual tem
a liberdade de utilizar as operações públicas do tipo abstracto. Note que o tipo do termo da
quarta linha é Bool ; nunca poderia ser Cont , ou qualquer outro tipo envolvendo Cont, pois o nome local Cont não deve poder escapar da construção open: as regras do sistema de tipos impõem
esta restrição.
2.5.4.2 Tipos existenciais restringidos
O tipo abstracto do exemplo anterior é completamente abstracto pois nada revela aos clientes
sobre a sua representação interna. Em [CW85] consideram-se também tipos parcialmente
abstractos, que revelam aos clientes parte da sua representação interna. Esses tipos são representados por tipos existenciais restringidos (bounded existential types). No seguinte exemplo
de tipo existencial restringido:
∃ Cont≤τ.{zero:Cont, inc:Cont→Cont, eq:Cont→Cont→Bool}
os clientes deste tipo ficam a saber que o tipo da representação interna é subtipo de τ, apesar
de continuarem a desconhecer qual é exactamente esse tipo interno. Note que se τ for um tipo-registo, então todas componentes de τ ocorrem também em Cont , eventualmente sob uma forma mais especializada.
2.5.4.3 Tipos existenciais F-restringidos
Neste ponto introduzimos a forma, ainda mais geral, de tipos existenciais F-restringidos, nos
quais o nome do tipo abstracto pode ser usado na sua própria restrição. Fazemos seguidamente
a codificação destes tipos usando as formas primitivas de F+ e apresentamos ainda as principais regras do sistema de tipos que lhes estão associadas (são deriváveis das regras primitivas).
A codificação dos tipos existenciais F-restringidos em formas primitivas é a seguinte:
∃ X≤ϕ[X].ρ =ˆ ∀ Y:Κ.(∀X≤ϕ[X].ρ→Y)→Y) onde Y∉FV(ϕ) e Y∉FV(ρ)
pack X≤ϕ[X]=σ with r =ˆ λY:Κ.λP:(∀ X≤ϕ[X].ρ→Y).P[σ]r
open e:∃ X≤ϕ[X].ρ as X,r in e′:υ =ˆ e[υ](λX≤ϕ[X].λx:ρ.e′)
Esta codificação adapta de forma imediata a codificação de tipos existenciais restringidos
apresentada em [Car97, GP96].
34
OM – Uma linguagem de programação multiparadigma
Apresentamos no final desta subsecção as regras do sistema de tipos de F+ relacionadas
com os tipos existenciais F-restringidos. Produzimos apenas alguns breves comentários sobre
elas:
A regra [Tipo ∃ ] introduz os tipos existenciais F-restringidos.
A regra [Sub ∃] estabelece que a relação de subtipo entre tipos existenciais requer uma forma de covariância generalizada da restrição sobre o tipo-representação (ao contrário do que se
passa com a restrição sobre o argumento dos tipos universais – regra [Sub ∀ ]) e covariância
normal no tipo do resultado (exactamente como nos tipos universais). Note que enquanto os
termos com tipos universal – as funções polimórficas – se destinam a ser aplicados, os termos
com tipo existencial – os pacotes – se destinam a ser abertos.
A regra [Termo ∃ ] descreve a definição dum pacote. Note como no segundo antecedente da
regra todas as ocorrências do nome público X são substituídas pelo tipo tipo-representação σ.
A regra [Termo open ∃] descreve a abertura dum pacote e . Dentro da regra, e′ denota o termo
cliente do pacote, υ o seu tipo, e ρ o registo das operações públicas do pacote e . O segundo
antecedente da regra faculta ao cliente e′ acesso ao nome X e às operações definidas no pacote.
O terceiro antecedente, Γ υ:∗, obriga υ a ser um tipo completamente independente de X: assim
X é forçado a ser local à construção open não podendo escapar para o exterior.
[Tipo ∃]
Γ‚X≤ϕ[X] ρ:∗
Γ
∃ X≤ϕ[X].ρ:∗
[Termo ∃ ]
Γ σ≤ϕ[σ] Γ r[σ/X]:ρ[σ/X]
Γ (pack X≤ϕ[X]=σ with r):∃ X≤ϕ[X].ρ
[Sub ∃ ]
Γ‚X≤ϕ[X] X≤ϕ′[X] Γ‚X≤ϕ[X] ρ≤ρ′
Γ ∃ X≤ϕ[X].ρ:∗
Γ
∃ X≤ϕ[X].ρ≤ ∃X≤ϕ′[X].ρ′
[Termo open ∃ ]
Γ e:∃ X≤ϕ[X].ρ Γ‚X≤ϕ[X]‚r:ρ e′:υ Γ υ:∗
Γ (open e as X,r in e′):υ
2.5.5 Concatenação de registos
Nesta secção, introduzimos uma operação tipificada de concatenação de registos. Em capítulos posteriores, usaremos esta operação na formalização de herança e de modo. Esta operação
consegue ser expressa usando as limitadas primitivas sobre registos introduzidas em F +.
Usando a nomenclatura de [Rém92], a operação de concatenação que vamos introduzir é
uma operação de concatenação assimétrica pois admite componentes homónimas nos registos
a concatenar. Uma operação de concatenação simétrica só admite concatenar registos sem
componentes homónimas.
Definimos da seguinte forma o nosso operador de concatenação assimétrica ⊕ de tipos-registo:
2 O sistema F+
35
Definição 2.5.5-1 (Concatenação de tipos-registo) Dados os tipos-registo
Π
e Π′ ,
definimos o tipo-registo concatenação Π⊕Π′ através da seguinte regra:
Π⊕Π′
=ˆ
Π={l1 :τ1 ‚…‚lm:τm‚h1 :υ1 ‚…‚h k :υk }
Π′={h 1 :υ′1 ‚…‚h k :υ′k ‚l′ 1 :τ′1 ‚…‚l′ n :τ′n }
{l1 :τ1 ‚…‚lm:τm‚h1 :υ′1 ‚…‚h k :υ′k ‚l′ 1 :τ′1 ‚…‚l′ n :τ′n }
Nesta regra, as componentes homónimas dos tipos-registo são identificadas pela letra h e as
componentes não homónimas pelas letras l e l′ .
Introduzimos agora a nossa operação de concatenação assimétrica de registos, que
representaremos através do operador binário +[Π‚Π′] .
Definição 2.5.5-2 (Concatenação de registos) Dados os tipos-registo
Π
e Π′,
definimos a operação de concatenação assimétrica +[Π‚Π′] como:
+[Π‚Π′]
=ˆ λr:Π.λr′:Π′. {l 1 =r.l1 ‚…‚lm=r.lm‚h1 =r′.h 1 ‚…‚hk =r′.h k ‚l′ 1 =r′.l′1 ‚…‚l′ n =r′.l′n }
A assinatura deste operador é:
+[Π‚Π′]: Π→Π′→Π⊕Π′
O resultado de concatenar dois registos r:Π e r′:Π′ é um registo do tipo Π⊕Π′ contendo todas as
componentes estaticamente conhecidas de r e r′ . Caso existam componentes estaticamente
conhecidas que sejam homónimas, os valores das componentes do segundo registo-argumento
têm precedência sobre os valores das componentes do primeiro registo-argumento.
Note que esta definição usa, como pretendíamos, as poucas operações primitivas sobre
registos de F+: a operação de construção de registo e a operação de selecção de componente.
A linguagem F+ suporta polimorfismo de inclusão, o que permite aplicar o operador +[Π‚Π′]
a registos com mais componentes do que as explicitamente indicadas nos seus tipos. A definição daquele operador é clara quanto ao tratamento dessas componentes extra: o registo-resultado considera apenas as componentes explicitamente previstas nos tipos Π e Π′ , ignorando
(truncando) as componentes extra. Este tratamento é essencial para a boa definição do operador: se as componentes extra do segundo argumento não fossem ignoradas então elas teriam
precedência sobre as componentes do primeiro registo, o que faria com que certas combinações de registos-argumento produzissem registos-resultado incompatíveis com o tipo Π⊕Π′.
De qualquer forma, em todas as utilizações concretas do operador +[Π‚Π′], só teremos a necessidade de o aplicar a registos constantes e sem componentes extra. Estas circunstâncias particulares do uso de +[Π‚Π′] permitirão simplificar a escrita deste operador, já que a inferência
dos tipos Π e Π′ a partir dos registos-argumento se tornará imediata. Assim, usaremos o símbolo + como abreviatura de +[Π‚Π′] sempre que os tipos dos argumentos sejam óbvios.
Na modelização do mecanismo de herança, toda a subclasse será definida por concatenação
do registo das componentes da superclasse com o registo das suas componentes específicas.
36
OM – Uma linguagem de programação multiparadigma
Além disso, na redefinição de componentes herdadas, as componentes da subclasse têm prioridade sobre as componentes da superclasse. Assim, o operador de concatenação tem as características requeridas pelo tratamento da herança. No entanto, ele também será usado noutros
contextos, por exemplo na definição do mecanismo dos modos.
Não conhecemos da literatura nenhum operador de concatenação semelhante a +[Π‚Π′] , embora existam técnicas alternativas usadas na definição de herança. Terminamos esta secção
referindo brevemente as alternativas principais que snao referidas na literatura.
Em [CHC90] define-se uma operação de concatenação, denotada por “ with”, que preserva
todas as componentes dos registos argumentos; o problema da boa definição do operador em
presença de polimorfismo de inclusão é resolvido de forma drástica e simplista, retirando os
tipos-registos da relação de subtipo.
Nos trabalhos [Car84/88, ESTZ94, PT94] não se usa qualquer operação de concatenação:
as subclasses são obrigadas a redeclarar exaustivamente todas as componentes herdadas.
Nas abordagem não-tipificadas [CP89] e [KR93] um registo é tratado simplesmente como
uma função com domínio num conjunto de etiquetas, o que torna a operação de concatenação
de definição trivial. Nas abordagens tipificadas, o que torna os registos entidades complexas é
o facto do domínio de cada registo fazer parte do tipo do registo.
Finalmente em [Rém89, CM91] são introduzidos registos extensíveis sobre os quais é possível definir uma operação de concatenação poderosa que preserva todas as componentes dos
registos-argumentos, mesmo as componentes estaticamente desconhecidas. No entanto, esta
forma de concatenação exige um sistema de tipos com regras complexas em que os tipos
capturam não só informação positiva como também informação negativa: um tipo-registo é
caracterizado pelo conjunto das componentes que se sabe que possui e, ainda, pelo conjunto
das componentes que se sabe que não possui. Outras abordagens que usam registos extensíveis
usam row types da forma X↑{l1 ,…,ln }, representando colecções etiquetadas de tipos onde não
ocorrem l1 ,…,ln [Wan87, Wan89, Car94, FHM94]. De forma geral, estes complicados sistemas
de tipos pretendem resolver o clássico problema da actualização funcional (functional update)
em F≤ [CM91]: não é possível definir em F≤ uma função polimórfica do tipo ∀ X≤B.X→X que
efectue a actualização funcional (cópia modificada) de objectos do tipo X, para todo o X≤B.
Felizmente que no início dos anos 90 se descobriu uma forma simples de contornar este problema: basta definir as funções polimórficas sobre interfaces em vez de sobre tipos-objectos
[Bru94, PT94].
2.5.6 Referências
Para modelizar objectos contendo variáveis de instância mutáveis, iremos precisar de usar um
cálculo-lambda imperativo com referências e efeitos laterais. Assim vamos agora estender o
+
sistema F+ com os habituais ingredientes imperativos. Chamaremos ao novo sistema F&
.
2 O sistema F+
37
2.5.6.1 Sintaxe de F+&
+
Gramática dos novos tipos de F&
:
κϕτυσ ::=
(Formas derivadas)
…
| Ref υ
| Unit
:∗
:∗
tipo-referência
tipo cujo único elemento é ()
:Ref υ
:υ
:Unit
:Unit
:Unit
criação de localização
valor de localização
actualização de localização
sequenciação
único valor do tipo Unit
+
Gramática dos novos termos de F&
:
efP ::=
(Formas derivadas)
…
| ref (e:υ)
| deref e
| e:=e′
| e;e′
| ()
2.5.6.2 Regras de boa formação de F+&
Regras adicionais de boa formação de tipos:
[Tipo Ref]
Γ υ:Κ
Γ Ref υ:∗
[Tipo Unit]
Γ ◊
Γ Unit:∗
Regras adicionais de boa formação de termos:
[Termo ref]
Γ e:υ
Γ ref (e:υ):Ref υ
[Termo deref]
Γ e:Ref υ
Γ deref e:υ
[Termo ;]
Γ e:τ Γ e′:τ′
Γ e;e′:τ′
[Termo ()]
Γ ◊
Γ ():Unit
[Termo :=]
Γ e:Ref υ Γ e′:υ
Γ e:=e′:Unit
Não são necessárias novas regras de subtipo pois a única regra de subtipo aplicável aos tipos-referência é a regra da reflexibilidade.
2.5.6.3 Semântica de F+&
A nova linguagem imperativa tem efeitos laterais pelo que deixa de ser confluente. Este facto
sugere a escrita duma semântica operacional determinista como forma de definição semântica
do sistema. Contudo não faremos isto. Como até aqui, vamos a investigar a possibilidade de
+
reduzir as novas construções ao sistema base, neste caso, traduzindo os termos de F&
para termos funcionais que manipulam explicitamente uma representação concreta do estado. Numa
forma não-tipificada, esta técnica de tradução é usada com sucesso em [SF94], por exemplo.
38
OM – Uma linguagem de programação multiparadigma
Infelizmente, o grau de sucesso que a variante tipificada permite é apenas parcial, como veremos.
Tratemos em primeiro lugar do problema da representação explícita do estado.
É simples a concepção uma representação para estados que só permitam guardar valores
dum tipo único (estados monotipificados) (cf. [Sto77]): um estado monotipificado sobre um
+
tipo τ de F&
é uma mera função de localizações para valores do tipo τ . As localizações podem
ser representadas usando números naturais (codificáveis no sistema F cf. [BB85]). Assim, o
seguinte operador de tipo representa todos os estados monotipificados:
MonoStore
=ˆ ΛX..Nat→X
Por razões técnicas, o estado politipificado que vamos agora definir só permite guardar valores de um número finito de tipos. Consideremos uma godelização do conjunto (contável) dos
+
tipos saturados (tipos sem variáveis livres) de F&
, que atribua um índice numérico único a cada um desses tipos: τ1 , τ2 , …. Dado um número N arbitrário, representaremos os nossos estados politipificados usando o seguinte tipo:
Store
=ˆ µStore.MonoStore[τ1 ] × MonoStore[τ 2 ] × …×MonoStore[τN]
Este tipo define-se recursivamente para permitir o armazenamento no estado de valores que
dependam do próprio estado.
Para aceder a cada uma das componentes monotipificadas do estado, introduzimos os seguintes operadores de tipo e as seguintes abstracções:
SEL
UPD
SU
=ˆ ΛX. Store→MStore[X]
=ˆ ΛX. Store→MStore[X]→Store
=ˆ ΛX. {sel:SEL[X], upd:UPD[X]}
seli
updi
sui
=ˆ λs:Store.(s.i) : SEL[τ i ]
=ˆ λs:Store.λm:MStore[τ i ].<s.1, …, s.i-1, m, s.i+1, …, s.n> : UPD[τi ]
=ˆ { sel=seli , upd=upd i } :SU[τi ]
A função seli permite extrair a i-ésima componente do estado. A função updi permite alterar a
i-ésima componente do estado. O registo sui agrupa as duas funções anteriores.
Para manipular localizações e seus valores associados, definimos as seguintes abstracções:
get_val
set_val
alloc
=ˆ λX.λsu:SU[X]. λs:Store.λl.Nat. ((su.sel s) l)
=ˆ λX.λsu:SU[X]. λs:Store.λl.Nat.λv.X. (su.upd s ((su.sel s)|l→v))
=ˆ λX.λsu:SU[X]. λs:Store.λv.X. <l, set_val[X] su s l v> onde get_val[X] su l = free
A função get_val permite obter o valor duma localização, a função set_val permite alterar o valor
duma localização, a função alloc permite reservar e inicializar uma nova localização. Estas três
funções são parametrizadas em função de X, o tipo de base da localização a manipular, e de
su:SU[X], um registo de funções que permitem o acesso e modificação da componente monoti-
2 O sistema F+
39
pificada do estado que corresponde a X. Terminamos assim a discussão da representação do
estado.
Consideramos agora a codificação em F+ do novo tipo Unit e do seu valor (). Basta usar as
seguintes definições:
Unit
()
=ˆ ∀ X:∗.X→X
=ˆ λX:∗.λx:X.x
Relembramos que, de acordo com a discussão sobre parametricidade que efectuámos na secção 2.1.1, o tipo ∀ X:∗.X→X representa um conjunto singular em F +. Além disso, ∀X:∗.X→X
não tem subtipos estritos e o seu único supertipo estrito é Top(∗) .
Na definição da tradução . , é necessário, não só dar significado às novas construções imperativas, como também fazer a reinterpretação de todas as formas primitivas da linguagem
base. É também importante notar que as formas derivadas da linguagem base devem ser codificadas nas formas primitivas, antes de lhes ser aplicada a tradução.
x
λx:υ.e
fe
λX≤ϕ[X].e
P[τ]
– –
{l =e}
r.l
ref (e:τ)
deref e
e:=ë
e;ë
()
+
. : F&
→F+
=ˆ λs:Store.<x,s>
onde x variável
=ˆ λs:Store.<λ<x,s′>:υ×Store.( e s′),s>
=ˆ λs:Store.let <f′,s′>= f s in f′( e s′)
=ˆ λs:Store.<λX≤ϕ[X].λs′:Store.λz:SU[X].( e s′),s>
=ˆ λs:Store.let <P′,s′>= P s in P′[τ] s′ suτ
––––––––––––––––
=ˆ λs:Store.<{l–=λs′:Store.
e s′ }‚s>
=ˆ λs:Store.let <r′,s′>= r s in <r′.l,s′>
=ˆ λs:Store.let <v′,s′>= e s in alloc[τ] suτ s′ v′
=ˆ λs:Store.let <l,s′>= e s in <get_val s′ suτ l,s′> onde e: Ref τ
=ˆ λs:Store.let <l′,s′>= e s in
onde e: Ref τ
let <v′′,s′′>=<ë,s′> in <(),set_val s′′ su τ l′ v′′>
=ˆ λs:Store.let <v′,s′>= e s in ë s′
=ˆ λs:Store.<(),s>
Nestas equações ocorre com frequência o termo suτ. O significado deste termo é o seguinte. Se
τ for um tipo saturado, então suτ=su i onde i é o índice numérico associado a τ pela godelização.
Se τ≡X for uma variável de tipo, então suX representa o registo de funções de acesso que foi
passado por parâmetro juntamente com a variável X.
Resta ainda o caso de τ ser um tipo não saturado diferente de variável de tipo simples. Como definir suτ neste caso? Infelizmente, neste ponto encontramos uma (previsível) limitação
essencial do esquema de tradução usado que não é possível contornar. A conclusão é que a solução que analisámos suporta apenas tipos referência de formas limitadas: tipos referência definidos sobre tipos saturados e tipos referência definidos sobre variáveis de tipo.
Os termos de F+ resultantes da tradução . podem ser avaliados usando uma qualquer estratégia de redução, visto a linguagem F+ ser confluente (embora umas estratégias possam ser
mais poderosas do que outras, já que F+ não satisfaz a propriedade da normalização forte). No
40
OM – Uma linguagem de programação multiparadigma
entanto, a escrita de . obriga a tomar decisões sobre o tratamento do estado (argumento s),
as quais induzem automaticamente uma certa ordem de avaliação ao nível da linguagem im+
perativa F&
. Optámos pela estratégia call-by-value pela razão habitual: a sequencialização das
operações sobre o estado facilita o raciocínio sobre este. Convém, no entanto, não esquecer
uma conhecida consequência desta escolha: o operador fix, que na linguagem original está
definido para todas as funções do tipo τ→τ, na linguagem com estratégia call-by-value está
definido apenas para funções com o tipo, mais específico, (υ→τ)→(υ→τ) (ver, por exemplo,
[Sch94] pág. 181, ou [Sto77] pág. 68).
+
Relativamente ao sistema de tipos de F&
, note que por efeito da tradução todos os termos
passam a ter tipos muito complicados. Por exemplo, assumindo que o termo e tem tipo τ em
F+, temos:
λx:υ.e : Store→((υ×Store→τ×Store)×Store)
f e : Store→τ×Store
λX≤ϕ[X].e : Store→(( ∀X≤ϕ[X].Store→τ×Store)×Store)
+
Contudo, é possível continuar a usar em F&
as regras do sistema de tipos de F +, conjuntamente com as regras introduzidas nesta secção, pois as múltiplas ocorrências de Store são vácuas do ponto de vista da verificação da boa tipificação dos programas (por exemplo, o tipo
υ×Store→τ×Store é válido sse o tipo υ→τ for válido).
2.5.7 Constante polimórfica nil
Nesta secção, introduzimos a constante polimórfica nil, uma constante atómica compatível
com qualquer tipo-registo. A constante nil será usada na inicialização implícita das variáveis de
instância de tipos-registo.
A constante nil irá permitir-nos resolver três problemas:
• Na linguagem L7&, os problemas técnicos ligados à geração de objectos com variáveis
de instância (cf. secção 7.3.2.2) podem ser resolvidos fazendo com que todas as variáveis de instância de tipos-registo sejam pré-inicializadas com o valor nil (cf. secção
7.3.2.4).
• No contexto da linguagem L7&, dado um tipo-objecto recursivo R que contenha uma
ou mais variáveis de instância desse mesmo tipo R, todos os objectos do tipo R terão de
ser, à partida, infinitos: realmente um objecto do tipo R tem de conter objectos do tipo
R , os quais, por sua vez, têm de conter objectos do tipo R , e assim sucessivamente. Ao
ser usada como constante terminal, a constante nil permite a criação de objectos finitos.
• Considerando uma variável de tipo-registo, R, introduzida numa função polimórfica da
linguagem L6, e.g. λR≤{}.…, não existe à partida qualquer valor que possa ser usado na
inicialização das variáveis do tipo-registo R dentro da abstracção. A constante nil resolve o problema pois é compatível com qualquer tipo-registo.
2 O sistema F+
41
Os tipos-registo originais de F+ não suportam directamente uma constante nil com as características pretendidas. Por isso, precisamos de introduzir novos registos, isomorfos aos primeiros na medida do possível, e compatíveis com alguma definição razoável para nil.
Eis uma possibilidade. A nossa codificação dos novos registos (a negro) em F+ é a seguinte:
––
{l :τ}
– –
{l =e}
e.l
=ˆ Unit→{l–:τ–}
=ˆ λz:Unit.{l–=τ–} : Unit→{l–:τ–}
=ˆ (e ()).l
A codificação de nil é a seguinte:
nil{l–:τ–}
=ˆ λz:Unit.diverge {l–:τ–} : Unit→{ –l :τ–}
Tecnicamente, esta última definição introduz uma família de constantes. Do lado direito da
equação, diverge{l–:τ–} representa a computação divergente de tipo {l–:τ–} (cf. secção 2.3.3.1). Note
que qualquer tentativa de acesso a uma eventual componente l de nil conduz, operacionalmente, a uma computação divergente, o que significa que o valor nil pode ser considerado atómico, já que não há qualquer estrutura nele detectável.
2.6 Modelo semântico
Fazemos aqui uma breve descrição do modelo semântico de Bruce e Mitchell [BM92], que
adoptámos como referência para a selecção dos ingredientes de F+. Este modelo de Bruce e
Mitchell providencia uma base semântica para linguagens com subtipos, tipos recursivos e polimorfismo paramétrico F-restringido de ordem superior.
Este modelo expande outros modelos, menos completos, anteriormente desenvolvidos:
[Cop85, Ama91, Car89, AP90]. Em todos eles, os tipos são interpretados como relações de
equivalência parciais (relações simétricas e transitivas) sobre um modelo D ∞ do cálculo-lambda não-tipificado. Este modelo é construído usando a tradicional técnica do limite inverso de
Scott [Sco72, Sto77]. D∞ é uma ordem parcial ω-completa. Portanto, todas as cadeias ascendentes {di }i∈ω de elementos de D∞ têm supremo sup{di }, todos os elementos de D ∞ são funções
contínuas (uma função f é continua se sup{f di }=f sup{di }) e cada uma destas funções tem um
ponto fixo mínimo (de acordo com o teorema do ponto fixo [Sco72, Sto77]).
Um termo é interpretado em D∞ como uma função contínua. Isto significa que a informação
de tipo incluída no termo é ignorada: o termo é tratado como se fosse um termo-lambda não-tipificado. Esta interpretação não-tipificada dos termos é característica das linguagens paramétricas, linguagens em que o comportamento das funções polimórficas não depende do tipo
dos elementos às quais elas são aplicadas [Str67, Rey83, Wad89, BL90, MR91, ACC93,
CMMS94]. Em [HP95, Cas96] estudam-se variantes não-paramétricas do sistema F≤.
Quanto aos tipos, eles são interpretados no modelo como relações de equivalência parcial
sobre D ∞, satisfazendo certas condições particulares que aqui não referiremos: assim, todos os
42
OM – Uma linguagem de programação multiparadigma
termos dum mesmo tipo estão em relação. O género ∗ dos tipos é modelizado usando uma
colecção de relações de equivalência parcial ℜ. ℜ é a menor colecção de relações de equivalência parcial contendo a relação mínima – {<⊥,⊥>} – e fechada para os construtores de tipo:
função, registo e quantificação F-restringida. A relação de subtipo da linguagem é interpretada
no modelo como a relação de inclusão entre elementos de ℜ.
A parte menos standard do modelo diz respeito à interpretação dos géneros (kinds). Os autores do modelo introduzem as noções de função promotora (rank-increasing function) e de
conjunto escalonado (rank-ordered set). No modelo, cada género é interpretado como um conjunto escalonado e cada operador de tipo como uma função promotora. Em [BM92], começa-se por verificar que ℜ é um conjunto escalonado e que todos os construtores de tipo vistos
como operadores de tipo são funções promotoras em ℜ. Define-se, então, ℑ a colecção de conjuntos escalonados que irá modelizar o conjunto de todos os géneros: ℑ é a menor colecção de
conjuntos escalonados contendo ℜ e fechada para as operações de ⇒ e × , onde κ⇒κ′ é o conjunto (escalonado) de todas as funções promotoras definidas entre os conjuntos escalonados κ
e κ′, e κ×κ′ é o habitual produto cartesiano, o qual é um conjunto escalonado. Toda esta construção garante que toda a função promotora definida sobre um conjunto escalonado tenha um
ponto fixo único. Este facto permite a definição de tipos recursivos ao nível de qualquer género.
Capítulo 3
Linguagem sem objectos
Introduzimos, no capítulo corrente, a linguagem L3, a primeira duma sequência de linguagens
abstractas, progressivamente mais ricas, que culminará na linguagem L10 do capítulo 10. A
linguagem concreta final OM, objectivo desta tese, será depois definida com base na linguagem L10. Como veículo de expressão semântica para a definição destas linguagens usaremos
F+, o cálculo-lambda polimórfico de ordem superior introduzido no capítulo anterior1 .
Introduziremos cada linguagem abstracta como uma extensão própria de F+. Portanto, as
construções específicas de cada uma delas serão definidas como formas derivadas de F+. Para
isso usaremos, como no capítulo anterior, regras da forma:
nova_construção
=ˆ esquema de codificação
Uma primeira consequência desta metodologia é o facto das várias linguagens nascerem
mergulhadas em F+. Por outras palavras, todas as construções de F+ serão também construções
das novas linguagens. Caso esta contaminação seja indesejada, o problema pode resolver-se
associando a cada linguagem uma gramática livre de contexto que, selectivamente, produza
apenas as construções pretendidas. Usaremos esta técnica tanto em L3 como nas linguagens
introduzidas nos próximos capítulos.
Outra consequência deste método é o facto de, através das codificações das novas construções, o sistema de tipos de F+ determinar implicitamente o sistema de tipos de cada uma das
novas linguagens. Esta consequência tem uma vantagem importante: o facto de não ser necessário explicitar autonomamente o sistema de tipos de cada uma das novas linguagens. Mas tem
também uma desvantagem: o sistema de tipos que resulta automaticamente das codificações
pode ser mais liberal do que o desejado. Este último problema, resolve-se introduzindo regras
de tipo complementares que imponham restrições adicionais à linguagem. De qualquer forma
não teremos necessidade de usar esta técnica, nem em L3, nem nas linguagens subsequentes.
A linguagem L3, a que vamos dedicar a secção seguinte, é apenas uma linguagem
preliminar que estabelece um ponto de partida para as linguagens dos próximos capítulos.
1 Mais exactamente, na codificação das construções das linguagens usaremos F+, a versão decidível do cálculo, e nas demond
strações usaremos a versão original indecidível do cálculo (cf. secção 2.3.5.6). No entanto, para simplificar o discurso, ao longo desta tese usaremos sempre o nome F + para designar ambas as versões do cálculo.
44
OM – Uma linguagem de programação multiparadigma
Nas gramáticas das várias linguagens abstractas, usaremos as seguintes convenções de
nomeação de entidades: τ,υ:∗ representam tipos simples, ϕ:∗⇒Κ um operador de tipo definido
sobre tipos simples‚ ϒ um tipo-registo, I uma interface, f:υ→τ uma função simples, e:τ um
termo de tipo simples‚ c uma classe, o um objecto, m um modo, R um registo, P: ∀X≤ϕ[X].τ um
termo abstracção de tipo, p um programa.
Usaremos ainda as duas seguintes convenções gráficas: (1) o tipo negro (bold) será usado
para assinalar tipos ou termos que, relativamente à linguagem anterior na sequência de
linguagens abstractas, sejam introduzidos pela primeira vez ou cuja semântica sofra alteração;
(2) o tipo itálico será usado para assinalar tipos técnicos e termos técnicos, entidades auxiliares
introduzidas na linguagem por razões técnicas. Os tipos e termos técnicos não fazem parte das
linguagens, tal como se pretende que elas sejam vistas pelo programador.
3.1 A linguagem L3
Sintaxe dos géneros,tipos e termos de L3
Κ ::= ∗ | ∗⇒Κ
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
lτ ::= <literais dos tipos Bool,Nat>
θ τ ::= <operações predefinidas sobre Bool,Nat>
p ::= prog(e)
τυϕϒI::= Bool | Nat | υ→τ | X |
Relações
τ≤τ′ subtipo
τ=τ′ equivalência de tipos
e=e′ equivalência de termos
Semântica dos termos
ˆ e
prog(e) =
A linguagem L3 é a nossa primeira aproximação à linguagem final OM, ou seja, é a primeira
linguagem abstracta que introduzimos como antecessora da linguagem OM. A linguagem L3 é
simples, não incluindo ainda ainda qualquer suporte para mecanismos de orientação pelos objectos.
3.1.1 Sintaxe
Os géneros de L3 são os seguintes: o género atómico dos tipos monomórficos ∗, e os géneros
estruturados ∗⇒∗, ∗⇒∗⇒∗, ∗⇒∗⇒∗⇒∗ , etc. Portanto, os géneros de L3 são mais limitados do
que os géneros de F+.
3 Linguagem sem objectos
45
Os tipos de L3 são os seguintes: tipos funcionais υ→τ, tipos-registo {l–:τ–}, operadores de tipo
ΛX.κ, instanciações de operadores de tipo ϕ[τ] , tipo primitivo Bool, tipo primitivo Nat . Estes
dois últimos primitivos últimos são codificáveis em F+ usando as técnicas de [CMMS94],
págs. 23 e 24. Os tipos Bool e Nat ser-nos-ão úteis na apresentação de exemplos práticos com
algum realismo.
Note que a linguagem L3 não inclui tipos recursivos µX.κ. No entanto, das próximas linguagens abstractas introduzirão tipos recursivos de formas restritas. É o caso da linguagem L4,
que introduz tipos-objecto recursivos, codificados em F+ usando µX.κ .e tipos-registo.
Os termos de L3 são os seguintes: abstracções monomórficas λx:υ.e, suas aplicações f e, registos {l–=e–}, valores recursivos rec x:τ.e, literais primitivos lτ sobre Bool e Nat (false, true, 0, 1, 2, 3,
4, 5, …), operações primitivas θτ sobre Bool e Nat (and, or, if-then-else, …, +, -, *, /, =, … ). Não
entraremos no detalhe da especificação destes literais e operações primitivas.
3.1.2 Relações binárias
Na linguagem L3, e também nas linguagens L4, L5, etc, vamos assumir a existência das seguintes três relações sobre tipos e termos, todas herdadas do sistema F+:
• relação de subtipo;
• relação de equivalência entre tipos;
• relação de equivalência entre termos.
Para todos os novos tipos e termos a introduzir das próximas linguagens abstractas, estas
três relações serão inferidas, considerando a codificação desses tipos e termos em F +.
3.1.3 Programa
Um programa em L3 tem a forma prog(e), onde e é uma expressão simples. O valor de prog(e) é
o valor da expressão e. Note que se exceptuarmos a nova construção prog(e), a linguagem L3 é
uma simples restrição do sistema F+. Tal deixará de ser verdadeiro a partir da próxima linguagem, L4.
Vamos permitir que um programa possa usar nomes previamente introduzidos ao nível da
meta-linguagem. Num certo sentido essas mesmas definições farão parte do programa. Eis um
exemplo dum tal programa de L3, onde diversos nomes (metavariáveis) são introduzidos:
AutoFun
NatFun
inc
apply
p
=ˆ
=ˆ
=ˆ
=ˆ
=ˆ
ΛX.X→X
AutoFun[Nat]
λx:Nat.x+1
λf:NatFun.λx:Nat.(f x)
prog(apply inc 0)
:∗⇒∗
:∗
:Nat→Nat
:(Nat→Nat)→Nat→Nat
:Nat
46
OM – Uma linguagem de programação multiparadigma
Note como neste programa o operador de tipo AutoFun, introduzido na primeira linha, é usado para definir o tipo monomórfico NatFun, na segunda linha. Note ainda como são introduzidos os termos inc e apply, e como são estes usados no corpo do programa, na última linha.
O valor deste programa é 1. Este facto pode ser verificado usando as regras de identidade
entre termos de F + da secção 2.4.1.
Capítulo 4
Objectos simples com herança
Sintaxe dos géneros,tipos e termos de L4
Κ ::= ∗ | ∗⇒Κ
τυϕϒI::= Bool | Nat | υ→τ | X |
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′ |
SAMET | CLASSTYPE(ϒ) | INTERFACE(ϒ) | OBJTYPE(ϒ)
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
self | super | class R | class\s R | new c | o.l |
checkType[τ] | downcastσ[τ]
Semântica dos tipos
ˆ
INTERFACE(ϒ c) :∗ =
interface
ϒc
ˆ
OBJTYPE(ϒ) :∗ =
tipo-objecto
µSAMET.ϒ (= ϒ[OBJTYPE(ϒ)/SAMET])
ˆ
CLASSTYPE(ϒ c) :∗ =
tipo-classe
OBJTYPE(ϒ c)→OBJTYPE(ϒc)
Semântica dos termos
ˆ
checkType[τ] :τ→Bool =
ver secção 4.3.5
ˆ
downcastσ[τ] :σ→τ =
ver secção 4.3.5
class Rc :CLASSTYPE(ϒ c)
=ˆ
λself:OBJTYPE(ϒ c).
Rc
class\s Rc :CLASSTYPE(ϒ s⊕ϒc)
let S:CLASSTYPE(ϒ s) = s in
=ˆ
λself:OBJTYPE(ϒ s⊕ϒc).
let super:OBJTYPE(ϒ s) = (S self) in
super+Rc
*Restrição implícita: ϒ s,ϒ c devem ser tais que:
OBJTYPE(ϒ s⊕ϒc)≤OBJTYPE(ϒ c)
ˆ
new c :OBJTYPE(ϒ c) =
let gen:CLASSTYPE(ϒ c) = c in
let o:OBJTYPE(ϒ c) = fix gen in
o
ˆ
o.l :τ =
let R:OBJTYPE(ϒ c) = o in
R.l
Na secção 4.1, começamos por introduzir informalmente os conceitos e mecanismos essenciais
da linguagem L4. A formalização da linguagem é depois efectuada na secção 4.2, onde
também determinamos os requisitos de boa tipificação das equações semânticas. Na secção
48
OM – Uma linguagem de programação multiparadigma
4.3, discutimos o potencial de utilização prática da linguagem L4. Fazemos também o levantamento de diversos problemas que atingem a linguagem e tentamos encontrar soluções para
eles no estrito âmbito de L4, sem estender a linguagem. Na secção 4.4 damos um pouco de
perspectiva sobre a linguagem L4 e relacionamos o material deste capítulo com trabalhos de
outros autores.
4.1 Conceitos e mecanismos de L4
Nesta primeira secção do capítulo 4 descrevemos informalmente os conceitos e mecanismos
da linguagem funcional L4, que não são mais do que versões dos mecanismos tradicionais das
linguagens orientadas pelos objectos: objecto, classe, herança e subtipo. A descrição é sumária pois a forma que estas noções assumem em L4 não difere muito da forma que elas assumem em linguagens amplamente divulgadas, como o Smalltalk [GM83, Kra83], C++ [ES90],
Modula-3 [CDJ+89], Java [Sun95, AG98], Pizza [OW97], Eiffel [Mey88, Mey92], ou Sather
[Omo92].
Ao longo deste capítulo e seguintes, tentaremos seguir a nomenclatura da linguagem
Smalltalk, no que diz respeito a construções e mecanismos orientados pelos objectos.
4.1.1 Objectos
Os objectos de L4 são termos compostos definidos recursivamente, com acesso a si próprios
através do nome ligado self. Cada objecto é caracterizado por um conjunto de expressões etiquetadas e não mutáveis chamadas métodos. Um método pode ser seleccionado do objecto a
que pertence, usando a sintaxe o.m , ficando assim disponível para ser avaliado.
Envio de mensagem é o nome sugestivo dado à composição das operações de selecção e de
avaliação: a dita mensagem é a etiqueta m usada para seleccionar o método e a resposta à
mensagem é o resultado da avaliação do método. Note que, em L4 e linguagens seguintes, o
envio da mesma mensagem m para dois objectos do mesmo tipo pode resultar na selecção e
avaliação de métodos distintos. Isto mostra que só em tempo de execução é possível estabelecer a ligação entre uma mensagem m e o método que ela selecciona. Esta é pois uma ligação
dinâmica.
Os objectos de L4 são formalizados usando registos de F+. Para exemplificar, eis um objecto concreto de L4:
ob
=ˆ {x=5, y=8, sum=self.x+self.y}
A linguagem L4 é puramente funcional, i.e. não tem estado mutável. Além disso, os seus
objectos só possuem parte pública. A secção 4.3.1 mostrará que, apesar destas limitações, é
possível escrever em L4 programas não triviais e com utilidade prática.
4 Objectos simples com herança
49
Ao contrário de L4, a linguagem OM final suportará objectos mutáveis (i.e. objectos com
variáveis de instância mutáveis), e também componentes privadas, protegidas por uma barreira de encapsulamento. Sendo estes mecanismos elementares (pelo menos aparentemente), poderá ser questionada a razão deles não serem, desde já, incluídos na linguagem L4. A justificação é a seguinte: tratam-se, afinal, de elementos cujo tratamento é complexo e artificioso, o
que interfere negativamente na clareza da formalização de outros aspectos da linguagem, para
nós de estudo mais prioritário. A introdução e estudo destes dois mecanismos fica adiada para
o capítulo 7.
Convém, porém, antecipar um pouco de terminologia. Designaremos, de forma genérica,
por componentes dum objecto, a colecção das variáveis de instância e dos métodos desse objecto. Quanto à separação entre componentes privadas e componentes públicas, esta resulta
duma partição arbitrária que o programador tem a possibilidade de efectuar sobre o conjunto
de componentes de qualquer objecto. Por definição, as componentes privadas só são acessíveis
a partir dos métodos do próprio objecto (usando o nome self); já as componentes públicas podem ser acedidas por quem tiver acesso ao objecto, incluindo o próprio objecto.
4.1.2 Tipos-objecto
Um tipo-objecto regista o nome e tipo das várias componentes que integram os objectos desse
tipo. Portanto, um tipo-objecto descreve a estrutura de uma colecção de objectos; não a sua
implementação.
Em L4, um tipo-objecto tem a forma OBJTYPE(ϒ) e pode ser recursivo na variável de recursão SAMET (SAMET é uma abreviatura de same type). No tipo OBJTYPE(ϒ), a letra grega ϒ representa o tipo-registo das componentes do tipo-objecto.
Eis um exemplo de tipo-objecto não-recursivo, escrito usando a sintaxe de L4:
PointT
=ˆ OBJTYPE({x:Nat, y:Nat, sum:Nat})
Eis agora um exemplo de tipo-objecto recursivo:
PT
=ˆ OBJTYPE({x:Nat, y:Nat, sum:Nat, eq:SAMET→Bool})
Usando a relação de equivalência entre tipos de L4, um tipo-objecto pode geralmente ser
reescrito de diferentes formas, equivalentes entre si. Por exemplo, este último tipo, PT, pode
ser reescrito permutando as componentes do tipo-registo nele incluído, ou aplicando a regra de
unfolding de tipos recursivos o número de vezes que for desejado.
4.1.3 Classes
Uma classe é uma estrutura sintáctica que descreve a implementação duma família de objectos.
50
OM – Uma linguagem de programação multiparadigma
Formalizamos uma classe como uma função geradora de objectos extensível. Os objectos
gerados por uma classe dizem-se instâncias dessa classe. Todas as instâncias duma classe têm
a mesma estrutura – i.e. pertencem ao mesmo tipo-objecto – e a mesma funcionalidade – i.e.
contêm as mesmas componentes semânticas. As componentes semânticas duma classe consistem no corpo dos métodos e, ainda, no valor inicial das variáveis de instância (a introduzir no
capítulo 7).
A formalização das classes usando funções geradoras de objectos é natural. Inclusivamente,
existem linguagens de programação, e.g. Beta [KMMN87], em que as classes são explicitamente tratadas como funções.
Como vimos em 4.1.2, na linguagem L4 os tipos-objecto são caracterizados unicamente por
informação estrutural. É assim possível e normal a ocorrência num programa de duas classes
com semânticas distintas (implementações distintas), mas gerando objectos do mesmo tipo.
Este dado significa que L4 separa os conceitos de classe e de tipo-objecto, seguindo as ideias
expostas em [CHC90, LP91] e adoptadas nas linguagens experimentais Emerald [BHJL86],
PolyTOIL [BSG95, BFSG98], LOOP [ESTZ94], LOOM [BPF97] e Moby [FR99]. Diverge,
porém, de linguagens, como o C++, Eiffel, Sather, nas quais um tipo-objecto é uma classe. Já
agora, a linguagem Java suporta as duas visões: em Java cada classe introduz um tipo-objecto
distinto; mas a linguagem suporta adicionalmente os chamados tipos-interface, que se baseiam
em informação estrutural.
Representamos por CLASSTYPE(ϒc) o tipo-classe das classes que têm ϒc como tipo-registo
das suas componentes. Por construção, uma classe de tipo-classe CLASSTYPE(ϒc) gera objectos
do tipo-objecto OBJTYPE(ϒc). Para exemplificar, tomemos a classe pointC:
pointC
=ˆ class { x=0, y=0,
sum=self.x+self.y,
eq=λa:PT.(self.x=a.x & self.y=a.y) }
que depende do tipo PT assim definido:
PT
=ˆ OBJTYPE({x:Nat,y:Nat,sum:Nat,eq:SAMET→Bool})
A classe pointC gera objectos do tipo PointT:
PointT
=ˆ OBJTYPE({x:Nat,y:Nat,sum:Nat,eq:PT→Bool})
e tem a interface PointI:
PointI
=ˆ INTERFACE({x:Nat,y:Nat,sum:Nat,eq:PT→Bool})
O tipo da classe pointC é PointCT, e escreve-se:
PointCT
=ˆ CLASSTYPE({x:Nat,y:Nat,sum:Nat,eq:PT→Bool})
Neste exemplo, verifica-se a igualdade de tipos PT=PointT . Por isso podemos dizer com segurança que o tipo PointT também é recursivo. Note como, no exemplo, o tipo-objecto PT foi
habilmente introduzido para que a classe gerasse um tipo-objecto recursivo: em L4, uma classe
4 Objectos simples com herança
51
não pode especificar directamente, i.e. sem usar um tipo auxiliar, que o tipo-objecto por ela
gerado é recursivo, embora em L5 já seja possível fazer isso.
Todos os objectos gerados por uma classe têm o mesmo tipo. Referir-nos-emos a esse tipo-objecto como o tipo-objecto gerado pela classe ou o tipo-objecto correspondente à classe.
4.1.4 Interfaces
Chamamos interface de classe, ou apenas interface, a toda a informação de tipo observável
numa classe. Essa informação de tipo é capturada de forma neutra, livre de qualquer interpretação prematura. Uma interface é assim, essencialmente, um repositório de informação não
tratada. As interfaces estão na base da formalização dos tipos-objecto e dos tipos das classes.
Em L4, numa interface não pode ocorrer o nome SAMET.
As interfaces são representados por tipos da forma INTERFACE(ϒc). Uma classe com interface INTERFACE(ϒc) gera objectos do tipo OBJTYPE(ϒc).
4.1.5 Herança, subclasses e superclasses
Uma característica essencial das classes é o facto de elas serem incrementalmente modificáveis. Usando o mecanismo da herança, o programador consegue criar de forma expedita uma
nova classe a partir de outra, definindo apenas as componentes da nova classe que devem ser
adicionadas ou modificadas relativamente à classe original. As componentes da classe original
que não forem redefinidas na nova classe são automaticamente herdadas por esta. Nesta situação, a classe que herda diz-se subclasse imediata da classe que fornece as componentes. Por
sua vez, esta diz-se superclasse imediata da primeira.
Num programa de L4, qualquer classe c que não tenha superclasse imediata pode ser raiz
duma hierarquia de classes: a hierarquia das classes definidas, directa ou indirectamente, a partir de c por meio de herança. O primeiro nível da hierarquia é constituído pela classe c apenas;
o segundo nível é constituído por todas as subclasses imediatas de c; o terceiro nível é constituído por todas as subclasses imediatas destas; e assim sucessivamente. Justifica-se, assim, a
generalização das noções de subclasse imediata e de superclasse imediata: define-se a relação
binária de subclasse como sendo o fecho reflexo-transitivo da relação de subclasse imediata e
define-se a relação binária de superclasse como sendo relação inversa desta.
Note que o mecanismo de herança torna as classes em entidades extensíveis: uma classe
passa a ser uma implementação parcial para um número potencialmente infinito de subclasses.
Para potenciar ao máximo as vantagens e oportunidades de reutilização de código, os métodos herdados devem ser reinterpretados no contexto das subclasses que os acolhem. Para isso,
é preciso alterar o significado de self no interior dos métodos herdados: se na classe original
self representa um objecto dessa mesma classe, já na subclasse self deve passar a representar
52
OM – Uma linguagem de programação multiparadigma
um objecto da subclasse. Esta reinterpretação de self nos métodos herdados é típica da generalidade das linguagem orientadas pelos objectos.
Em L4, o nome super encontra-se disponível no interior de toda a subclasse e permite o
acesso às versões originais das componentes presentes na superclasse imediata. Note que, também, no caso em que acedemos a uma componente da superclasse usando super, a componente
acedida é interpretada no contexto da subclasse (por exemplo, qualquer método da superclasse
acedido através de super é executado no contexto da subclasse, usando as versões das componentes disponíveis na subclasse). O nome super é particularmente útil para estender na subclasse a funcionalidade dum método definido na superclasse: basta redefinir o método na subclasse tendo o cuidado de invocar a versão anterior do mesmo método no ponto apropriado da
nova versão e através de super.
4.1.6 “Reutilização sem reverificação”
A reinterpretação dos métodos herdados nas subclasses levanta a questão prática da “reutilização sem reverificação”: será que é possível verificar os métodos apenas uma vez, no momento
em que são originalmente introduzidos, evitando ter de os reverificar (e recompilar) num novo
contexto sempre que são herdados? Esta é uma questão importante pois uma eventual resposta
negativa inviabilizaria a possibilidade de compilação separada de classes e a consequente capacidade de distribuir classes extensíveis sob a forma de código compilado.
Claramente, pretendemos que a resposta àquela pergunta seja “sim”. Para isso começamos
por impor uma restrição geral de boa formação sobre todas as subclasses imediatas duma classe genérica c: o tipo-objecto gerado por toda a subclasse imediata de c deve ser subtipo do
tipo-objecto gerado por c . Esta restrição vai de encontro ao nosso objectivo, pois faz com que
o tipo de self dentro de qualquer método seja mais geral do que o tipo-objecto gerado por todas
as subclasses que possam herdar o método directamente.
Mas temos de pensar também nas classes não-imediatas de c pois estas também herdam de
c. A questão que agora se coloca é a seguinte: será que a restrição imposta sobre as subclasses
imediatas é suficiente para garantir “reutilização sem reverificação”, também no caso das subclasses não-imediatas? Para ver isso tomemos três classes c, b e a , onde c é uma subclasse imediata bem formada de b, e b é subclasse imediata bem formada de a . A pergunta reformulada
para o caso de c , b e a fica assim: será que, usando o conhecimento disponível sobre c , b e a, se
pode garantir que a classe c é também uma subclasse (não-imediata) bem formada de a? A resposta será positiva se a restrição subjacente à noção de boa formação for transitiva. Mas nós já
sabemos que essa restrição é transitiva pois trata-se da relação de subtipo em F+. Portanto, a
resposta a esta última questão é “sim”.
O facto da relação de subtipo ser, já por si, reflexiva, e não apenas transitiva, é uma vantagem adicional. Desta forma, a relação de subclasse permanece reflexiva e transitiva, mesmo
depois de submetida às restrições de boa formação.
4 Objectos simples com herança
53
Note como foi possível codificar a restrição que preside à definição das subclasses (bem
formadas) duma dada classe usando uma relação binária sobre os tipos-objecto gerados por essas classes: a relação de subtipo, neste caso. Na linguagem L5 introduziremos uma relação de
herança mais geral que a de L4, mas também usando esta técnica de recorrer a uma relação
binária sobre os tipos-objecto gerados pelas classes.
Tal como L4, as linguagens C++ e Java suportam “reutilização sem reverificação”. Pelo
contrário, as linguagens Eiffel e Sather exigem “reutilização com reverificação”: são linguagens com mecanismos de herança muito liberais, onde chega mesmo a ser possível fazer herança selectiva de componentes usando uma directiva undefine: note que a herança selectiva é a
forma mais óbvia de violar a nossa restrição de boa formação.
4.1.7 Subtipos
A relação de subtipo em L4 é inferida considerando a codificação desses tipos em F+ e usando
as regras de F+ relativas a subtipos. Isto significa que, em L4, a relação de subtipo entre tipos-objecto assenta em informação estrutural, não dependendo de nenhuma forma de aspectos
relacionados com a implementação das classes. Assim, os conceitos de subtipo e subclasse estão separados em L4: mais precisamente, dois tipos podem estar na relação de subtipo sem que
as classes que lhes deram origem estejam na relação de subclasse. Neste aspecto L4 difere das
linguagens C++, Java e Eiffel, nas quais um subtipo se identifica com uma subclasse.
Agora que as noções de herança e subtipificação já foram introduzidas é conveniente chamar a atenção de que se tratam de noções distintas. A herança é um mecanismo de implementação que permite a reutilização de código – o que simplifica a escrita de programas e os torna
mais compactos, legíveis e modificáveis. Subtipificação é um mecanismo semântico que permite organizar em níveis de abstracção alguns conceitos usados nos programas: mais concretamente, permite tratar objectos dum tipo específico como se fossem objectos dum tipo mais
geral.
4.2 Semântica de L4
Esta secção, é dedicada à apresentação e discussão da formalização dos aspectos semânticos
de L4. O essencial da formalização da sintaxe e da semântica de L4 encontra-se na tabela apresentada no início do presente capítulo.
4.2.1 Semântica dos tipos
Em L4, a forma geral duma classe sem superclasses é class Rc, onde Rc representa o registo das
componentes da classe. Já uma subclasse imediata duma classe s tem a forma class\s R c. Supondo que Rc tem o tipo ϒc, apresentamos seguidamente a formalização de alguns tipos chave,
essenciais para o estabelecimento do modelo que desenvolvemos para L4. Todos estes tipos
54
OM – Uma linguagem de programação multiparadigma
acompanhar-nos-ão ao longo das próximas linguagens evoluindo em paralelo com as próprias
linguagens.
Representa a interface que captura toda a informação de tipo observável
na classe class Rc. Formaliza-se, simplesmente, usando o tipo-registo ϒc. Note que, em L4, é
proibida a ocorrência de SAMET numa interface.
INTERFACE(ϒc):
É um tipo-objecto recursivo constituído pelas componentes do tipo-registo ϒ.
No contexto de OBJTYPE(ϒ), o nome SAMET é interpretado como uma referência recursiva ao
próprio tipo-objecto. Formalizamos o tipo-objecto como µSAMET.ϒ, ou equivalentemente, como ϒ[OBJTYPE(ϒ)/SAMET] (usando unfolding). O tipo-objecto gerado por uma classe com interface INTERFACE(ϒc) é OBJTYPE(ϒc). Como o nome SAMET não pode ocorrer nas classes de L4,
SAMET também não pode ocorrer espontaneamente no tipo-objecto gerado por uma classe de
L4.
OBJTYPE(ϒ):
CLASSTYPE(ϒc):
Representa o tipo-classe das classes com interface INTERFACE(ϒc). Formalizamos este tipo usando OBJTYPE(ϒc)→OBJTYPE(ϒc), um tipo-gerador de objectos extensível.
A formalização do tipo-classe CLASSTYPE(ϒ c) requer uma explicação. Devido à reinterpretação de self nas subclasses, um gerador não se pode comprometer prematuramente com um significado para self. Por isso temos de considerar um gerador como uma entidade aberta, parametrizada em função do significado de self . Para self escolhemos o tipo OBJTYPE(ϒ c), porque
self vai representar qualquer um dos objectos que a classe gera. Para tipo do resultado do gerador escolhemos também OBJTYPE(ϒc), pois este é o tipo dos objectos a gerar. Assim o tipo-gerador pretendido será OBJTYPE(ϒc)→OBJTYPE(ϒc).
A escolha de OBJTYPE(ϒc) para tipo de self pode parecer prematura pois, com a mudança de
significado de self nas subclasses, talvez devêssemos considerar também a mudança do tipo de
self nas subclasses. No entanto a escolha que efectuámos é a única possível se tivermos por objectivo encontrar um modelo monomórfico para a linguagem L4. Esse será realmente o nosso
objectivo por agora, pelo que viveremos com as consequências desta escolha, em L4. Em L5,
self já terá uma tipificação mais sofisticada.
4.2.2 Semântica dos termos
Agora, discutimos a semântica das classes e subclasses de L4, e ainda a semântica das operações de criação de objectos e de acesso a componentes desses objectos. As equações semânticas encontram-se detalhadas na tabela que abre o capítulo corrente.
4.2.2.1 Semântica das classes
Na equação semântica correspondente à classe de L4 class Rc, o gerador que aí é introduzido é
realmente simples: ele limita-se a gerar um objecto com as componentes da classe e parametrizado relativamente a self. Não fora o problema da reinterpretação de self nas subclasses e seria
4 Objectos simples com herança
55
ainda possível aplicar o operador de ponto fixo ao gerador nessa mesma equação. Mas como
tal não é possível, a aplicação de fix fica adiada para o momento em que se criam os objectos,
por aplicação do operador new a uma classe. Note que esta definição faz implicitamente a validação do código dos métodos da classe: se o registo Rc estiver mal tipificado então o termo
class R c está mal tipificado.
O tratamento da herança está formalizado na equação do termo subclasse imediata
class\s Rc. Logo à partida, tanto a interface da subclasse como o tipo-objecto gerado pela subclasse são determinados através da combinação, usando o operador ⊕, do tipo-registo das componentes herdadas, ϒs , com os tipo-registo das componentes definidas na subclasse, ϒc. A
interface da subclasse é INTERFACE(ϒ s ⊕ϒc) e o respectivo tipo-objecto é OBJTYPE(ϒs ⊕ϒc). A
reinterpretação, e consequente mudança de tipo de self na subclasse, é efectuada da seguinte
forma. Primeiro, introduzimos um novo self do tipo dos objectos da subclasse. Seguidamente,
aplicamos o gerador S correspondente à superclasse ao esse novo self: desta forma ajustamos
os métodos da superclasse ao contexto da subclasse, o que é necessário para se ter efectiva
reutilização de código. Finalmente, modificamos o conjunto das componentes herdadas e já
adaptadas ao contexto da subclasse por aplicação do operador + . Assim criamos um novo gerador por extensão dum gerador existente.
Relativamente a super, este nome fica ligado a (S self), o que está conforme a descrição informal de super efectuada um pouco atrás. Repare no seguinte pormenor: um gerador não se compromete com um significado para self (self é um parâmetro dum gerador) mas, pelo contrário,
compromete-se com um significado para super (super=(S self)). É por esta razão que a tentativa
de explicar a herança como cópia textual de componentes falha no caso dos métodos herdados
que usam super.
4.2.2.2 Boa formação das subclasses
Para que o termo class\s R c esteja bem tipificado é necessário que o registo super+Rc esteja, também ele, bem tipificado. Mas existe outra questão de tipificação que é prévia a esta. Note que o
gerador S correspondente à superclasse está preparado para receber um objecto da superclasse,
portanto do tipo OBJTYPE(ϒs ). No entanto é aplicado, no termo
super = S self,
a um objecto da subclasse, portanto do tipo OBJTYPE(ϒs ⊕ϒc). Assim, outra condição necessária
para a boa tipificação das subclasses é que os tipos-registo ϒs , ϒc obedeçam à restrição:
OBJTYPE(ϒs ⊕ϒc)≤OBJTYPE(ϒc)
Esta condição indica que o tipo-objecto correspondente à subclasse deve ser subtipo do tipo-objecto correspondente à superclasse. Assim, podemos afirmar que em L4 “herança implica
subtipificação”, pois sem a verificação da restrição de subtipo não é possível ter herança neste
modelo. A implicação contrária não se verifica em L4, pois dois tipos-objecto podem estar na
relação de subtipo sem que as classes que os originaram estejam na relação de subclasse.
56
OM – Uma linguagem de programação multiparadigma
Ainda sobre a definição do termo class\s Rc, note que foi possível escrever a respectiva equação sem que fossem explicitadas as componentes da superclasse: foi apenas necessário explicitar o tipo das componentes da superclasse. É por esta razão que o mecanismo de herança de
L4 é compatível com a compilação separada de classes. Um mecanismo de herança irrestrito,
semelhante ao da linguagem Sather, teria de ser descrito usando uma definição semelhante à
que se segue:
class\(class Rs ) Rc :CLASSTYPE(ϒs ⊕ϒc) =ˆ
λself:OBJTYPE(ϒs ⊕ϒc).
let super:OBJTYPE(ϒs ) = R s in
super+Rc
4.2.2.3 Semântica dos outros termos
Vamos apresentar as definições das formas derivadas que restam por explicar.
O termo new c serve para criar objectos da classe c . De acordo com a equação semântica
respectiva, cada objecto é criado aplicando o operador de ponto fixo ao gerador de objectos
correspondente a c. O nome self fica ligado no contexto do próprio objecto.
O termo que permite o acesso às componentes dos objectos – o.l – é codificado num simples acesso a uma componente dum registo. Como já vimos, modelizamos os objectos usando
registos.
4.3 Discussão sobre L4
Para ilustrar as potencialidades e as limitações de L4 vamos usar ao longo desta secção, e respectivas subsecções, um exemplo que envolve a representação de pontos a duas e três dimensões, com estruturas definidas pelos tipo-objecto Point2T e Point3T, respectivamente. Apresentamos duas classes parametrizadas e recursivas, point2PC e point3PC, que implementam objectos
destes tipos, e indicamos quais as suas interfaces, Point2I e Point3I.
Point2T
Point2I
point2PC
=ˆ OBJTYPE({x:Nat,y:Nat, sum:Nat, id:SAMET, inc:SAMET,eq:SAMET→Bool})
=ˆ INTERFACE({x:Nat,y:Nat, sum:Nat, id:Point2T, inc:Point2T,eq:Point2T→Bool})
=ˆ rec p2. λa:Nat.λb:Nat.class {
x=a, y=b,
sum=self.x+self.y,
id=self,
inc=new (p2 (succ self.x) (succ self.y)),
eq=λa:Point2T.(self.x=a.x & self.y=a.y) }
Point3T
Point3I
point3PC
=ˆ OBJTYPE({x:Nat, y:Nat, z:Nat, sum:Nat, id:Point2T, inc:SAMET,eq:???→Bool})
=ˆ INTERFACE({x:Nat, y:Nat, z:Nat, sum:Nat, id:Point2T, inc:Point3T,eq:???→Bool})
=ˆ rec p3. λa:Nat.λb:Nat.λc:Nat. class\(point2PC a b) {
z=c,
sum=super.sum + self.z,
4 Objectos simples com herança
57
inc=new (p3 (succ self.x) (succ self.y) (succ self.z)),
eq=λa:???.(super.eq a & self.z=a.z) }
Nestas definições, usamos o símbolo ‘???’ para assinalar certas posições onde deveriam deveriam ocorrer tipos-objectos concretos. Atendendo às propriedades pretendidas para eq, não
existe substituição satisfatória para ‘???’ (cf. secção 4.3.4).
4.3.1 Inicialização dos objectos e criação de cópias
modificadas
Consideremos, em primeiro lugar, as duas seguintes restrições de L4: (1) todos os objectos
gerados por um classe são idênticos; (2) não há variáveis de instância mutáveis. Será possível,
com estas limitações, escrever programas não triviais que tenham utilidade prática? No caso de
L4 a resposta é positiva o que se deve à possibilidade de inicializar objectos e à possibilidade
de criar cópias modificadas de objectos usando classes parametrizadas definidas recursivamente.
Para exemplificar a inicialização de objectos, considere a expressão que cria o ponto zero,
new (point2PC 0 0) : a classe parametrizada point2PC é instanciada com os argumentos 0, 0 e,
depois, a classe não-parametrizada resultante é usada para gerar um objecto do tipo Point2T.
Quanto à possibilidade de criação de cópias modificadas, esta é uma consequência imediata da
possibilidade de inicializar objectos: observe como o método inc de point2PC, que tem a tarefa
de incrementar ambas as coordenadas dum ponto a duas dimensões, se limita a criar um novo
ponto inicializado com (succ self.x) e (succ self.y).
4.3.2 Problema da perda de informação
A linguagem L4 sofre do problema da perda de informação. Este problema, analisado em
[Car88], é típico de todas as linguagens que suportam a noção de subtipo, como é o caso das
linguagens Amber [Car86], C++, Java, Modula-3, etc. O problema pode ocorrer sempre que
um termo do tipo τ é promovido, por alguma razão, a um seu supertipo τ′. Como o conjunto de
operações suportadas pelo supertipo τ′ é frequentemente um subconjunto das operações suportadas por τ, o sistema de tipos, no seu conservadorismo, impede por vezes a aplicação ao
termo do tipo τ de operações para as quais ele estaria, afinal, preparado para responder.
Para exemplificar uma situação de perda de informação, considere o seguinte objecto do
tipo Point3T
ob
=ˆ new (Point3PC 1 2 3)
e ainda a função
ident
e a expressão
=ˆ λx:Point2T.x
58
OM – Uma linguagem de programação multiparadigma
(ident ob).z
A função ident pode ser aplicada ao objecto ob, pois o tipo deste é Point3T, um subtipo de
Point2T. Avaliando a expressão ident ob , obtemos o próprio objecto ob. Mas os resultados da
função ident têm um tipo fixo – Point2T –, pelo que o objecto ob produzido é estaticamente considerado como sendo um elemento do tipo Point2T. Como este tipo não prevê o método z , a
expressão (ident ob).z está mal tipificada.
A origem deste caso de perda de informação reside no facto do tipo do resultado da função
ident ser fixo, não dependendo do tipo do argumento da função. Usando uma construção de segunda ordem já seria possível resolver o problema. Bastaria reescrever a função ident da seguinte forma
ident
=ˆ λT≤Point2T.λx:T.x
e, depois, na invocação, indicar explicitamente qual o tipo pretendido para o resultado. É o
fazemos na expressão seguinte, a qual já está bem tipificada:
(ident[Point3T] ob).z
Assim, o problema da perda de informação desapareceria se eliminássemos da linguagem a
relação de subtipo e introduzíssemos polimorfismo de segunda ordem em sua substituição. No
entanto esta via não no satisfaz, pois há problemas que a relação de subtipo ajuda a resolver e
perante os quais o polimorfismo de segunda ordem se revela ineficaz (cf. secção 5.5.2).
Na realidade, o problema de perda de informação é insolúvel numa linguagem que suporte
a relação de subtipo e tenha um sistema de tipos puramente estático. O problema contorna-se
estendendo a linguagem com operações dinâmicas de teste e de despromoção de tipo (cf.
secção 4.3.5). Existem operações desta natureza – typecases, casts e downcasts – definidas na
maioria das linguagens que suportam a noção de subtipo. É o caso ds linguagens: Amber,
C++, Java e Modula-3.
4.3.3 Inflexibilidade na herança do tipo de self
Exemplificamos o problema da inflexibilidade na herança do tipo de self usando o método id,
introduzido na classe point2PC e herdado, mas não redefinido, em point3PC. Observe que este
método tem o tipo de self, ou seja Point2T, na classe em que é introduzido. O problema reside
no facto do método ser herdado sem que o tipo do seu resultado seja ajustado, apesar do tipo
de self ter mudado na subclasse.
Em resultado deste problema, pode surgir uma situação de perda de informação que é uma
variante da situação exemplificada no ponto anterior. Considere o seguinte objecto do tipo
Point3T:
ob
e a expressão:
=ˆ new (point3PC 1 2 3)
4 Objectos simples com herança
59
ob.id.z
Em resposta à mensagem id, o método id de point3PC devolve o mesmo objecto ob, mas agora visto como um termo do tipo Point2T. Mas como o tipo Point2T não prevê o método z , a
expressão ob.id.z fica mal tipificada.
Em L4, e também C++, Java, Modula-3, etc., a única forma de solucionar este problema é
insatisfatória: consiste em copiar manualmente o método da superclasse para a subclasse, sem
tirar partido do mecanismo de herança. Em L5, a introdução do tipo SAMET, representativo do
tipo de self dentro de cada classe, permitirá uma tipificação mais precisa das componentes e
resolverá definitivamente o problema da inflexibilidade na herança do tipo de self. A formalização de SAMET irá requerer a utilização de construções de segunda ordem.
4.3.4 Métodos binários
Consideramos agora o problema da definição de métodos binários [BCC+96]. Um método
binário é um método que tem pelo menos um parâmetro com o tipo de self. O método eq , introduzido na classe point2PC, é um exemplo de método binário.
A tentativa de redefinir eq na subclasse ilustra a dificuldade do tratamento de métodos binários em L4. O problema tem a ver com a tipificação do argumento de eq na subclasse. A solução intuitiva seria fazer ???≡Point3T, mas isso levaria a que Point3T deixasse de ser subtipo de
Point2T (cf. regra [Sub →] da secção 2.3.4.1), o que violaria a restrição fundamental que preside
à definição das subclasses: desta forma a subclasse ficaria mal tipificada. Podemos também
tentar ???≡Point2T, mas o método eq em point3PC fica agora com um argumento demasiado genérico, pelo que agora é o corpo do método que fica mal tipificado (o problema localiza-se exactamente na expressão a.z ). Efectuadas estas duas tentativas, concluímos que não é possível
encontrar qualquer substituição para ??? que nos permita evitar ter de reconfigurar o código
circundante. Esta é uma grave lacuna de expressividade de L4.
Em C++, Java, etc., a solução para este problema envolve a escolha de ???≡Point2T e o uso
de operações dinâmicas de teste e de despromoção de tipo, as quais permitem reescrever o corpo do método eq duma forma análoga à seguinte:
eq=λa:Point2T. super.eq a &
(if checkType[Point3T] a then self.z=(downcast Point2T[Point3T] a).z else false)
No trabalho [BCC+96] são apresentadas outras técnicas de definição de métodos binários,
mas as técnicas que são aplicáveis em L4 não são propícias ao uso mecanismo de herança:
uma técnica envolve a troca dos métodos binários por funções exteriores às classes; outra a
substituição dos métodos binários por colecções de métodos não binários. Exemplificando
com o caso do método eq, a primeira técnica substituiria eq pelas duas funções seguintes:
eqPoint2
eqPoint3
=ˆ λs:Point2T.λb:Point2T.(s.x=a.x & s.y=a.y)
=ˆ λs:Point3T.λb:Point3T.(eqPoint2 s b & s.z=a.z)
60
OM – Uma linguagem de programação multiparadigma
Em L4, não há realmente solução completamente satisfatória para o problema da definição
de métodos binários. Em L5, a introdução do nome de tipo SAMET nas classes permitirá resolver o problema de forma definitiva.
4.3.5 Tipos dinâmicos em L4
Já o referimos anteriormente, uma solução de recurso, que permite minorar as deficiências
dum sistema de tipos estático, consiste na introdução de operações dinâmicas de teste e de despromoção de tipo na linguagem. As linguagens Amber, C++, Java, Módula-3, Eiffel e Sather
são exemplos de linguagens que suportam tipos dinâmicos, em parte por esse motivo.
As deficiências do sistema de tipos de L4 justificam a introdução de tipos dinâmicos, também nesta linguagem. Na secção que agora se inicia, apresentamos uma técnica de introdução
de tipos dinâmicos em L4.
Repare que os tipos dinâmicos serão úteis, não só na linguagem L4, mas também nas futuras linguagens L5, L6, etc., incluindo a linguagem OM final. É certo que a maioria das deficiências de L4 se desvanecerá em breve, no contexto do sofisticado sistema de tipos de L5.
Contudo, um dos problemas de L4 – o problema da perda de informação (cf. secção 4.3.2) –
manter-se-á, sendo razão suficiente para a inclusão de tipos dinâmicos nas linguagens.
4.3.5.1 Introdução dos tipos dinâmicos
A introdução de tipos dinâmicos numa linguagem formalizada usando o sistema F+ é uma tarefa que não é imediata. O sistema F+ é paramétrico, pelo que está na sua essência a deliberada
abstracção de toda a informação de tipo em tempo de execução (cf. secção 2.1.1). Mas isso é
exactamente o oposto do que se pretende na nova versão de L4, uma versão que claramente
será não-paramétrica.
Assim, está fora de questão a codificação directa dos novos termos, e das novas operações
de teste e despromoção de tipo, como açúcar sintáctico sobre o sistema F+. Contudo, nada nos
impede de usar o sistema F+ como ferramenta para criar uma implementação duma nova versão dinâmica de L4. Vamos explorar esta ideia tentando identificar os aspectos chave da sua
realização.
Os únicos termos de L4 a que desejamos associar tipos dinâmicos são os objectos da linguagem. Por isso vamos construir, só para os objectos de L4, um sistema paralelo de representações explícitas de tipos-objecto dinâmicos.
Há dois aspectos a considerar.
Em primeiro lugar, há a questão do estabelecimento da associação física entre cada objecto
de L4 e o respectivo tipo dinâmico. Isso consegue-se através da inclusão automática, em todas
as classes de L4, duma componente específica chamada mytype, convenientemente inicializada.
4 Objectos simples com herança
61
Assim, cada objecto de L4 passa a contar com uma componente mytype que ficará ligada a uma
representação explícita do seu próprio tipo.
Em segundo lugar, há a questão da implementação da representação explícita dos tipos-objecto. Essa implementação pode, perfeitamente, ser realizada usando uma classe de L4,
digamos uma classe chamada $DYNAMIC_TYPE, geradora dum tipo-objecto $dyntype, e contendo uma implementação da relação de subtipo entre tipos-objecto num método chamado subtype.
Escrita a nova versão de L4, note que o sistema de tipos de F+ não nos dá quaisquer garantias de segurança e correcção. Efectivamente estes aspectos de L4 dependem agora de diversas questões exteriores a F+: Será que, nas classes de L4, a componente mytype é inicializada
com o tipo dinâmico certo, por referência ao tipo-objecto estático gerado? Será que a representação dos tipos dinâmicos captura fielmente as particularidades dos tipos estáticos de F+? Será
que o método subtype da classe $DYNAMIC_TYPE implementa correctamente a relação de subtipo de F +? Todas estas questões têm de ser resolvidas sem a ajuda do sistema F+. (Tais problemas não se colocaram na definição original de L4 porque o que estava aí em causa era uma
codificação, não uma implementação).
4.3.5.2 Operação de teste de tipo
Introduzida a representação explícita para tipos-objecto e assumindo os detalhes de implementação descritos na secção anterior, para cada tipo-objecto τ concreto escrevemos em F + a operação dinâmica de teste de tipo checkType[τ], como se segue :
checkType[τ] =ˆ λx:{mytype:$dyntype}. (x.mytype).subtype(explrep[τ]) :{mytype:$dyntype}→Bool
O termo explrep[τ] representa a instância da classe $DYNAMIC_TYPE que corresponde ao tipo-objecto τ . O tipo {mytype:$dyntype} é o tipo-objecto mais geral de L4.
A função checkType[τ] extrai o tipo dinâmico do argumento x, e verifica se ele é subtipo de τ,
usando para isso o método subtype da classe $DYNAMIC_TYPE.
4.3.5.3 Operação de despromoção de tipo
Relativamente à operação de despromoção, o objectivo é definir uma operação downcastσ[τ] que
ao ser aplicada a uma expressão e:σ, com τ≤σ, despromova o tipo estático dos seus resultados
de σ para τ sem afectar os valores produzidos. Como os resultados são para respeitar, a operação de despromoção só faz sentido nos casos em que os resultados são já elementos de τ – se
não o fossem, mudar o seu tipo de forma arbitrária não faria sentido.
Eis uma definição segura para downcastσ[τ], escrita em F+:
downcastσ[τ] =ˆ λx:σ. if checkType[τ] x then e else divergeτ
:<<σ→τ>>
Nesta definição divergeτ representa a computação divergente de tipo τ (cf. secção 2.3.3.1).
Quanto a if-then-else esta é uma primitiva booleana introduzida em L3.
62
OM – Uma linguagem de programação multiparadigma
A função produz simplesmente o seu próprio argumento, caso ele pertença ao tipo τ ; se não
pertencer, a função aborta por efeito do termo divergeτ (nesta situação, a linguagem OM geraria
uma excepção).
O sistema F+ atribui à função downcastσ[τ] o tipo σ→σ e não o tipo σ→τ como gostaríamos.
Realmente, em F+ não há nenhuma forma de tipificar a função da forma que pretendíamos: a
única regra que permite mudar o tipo dum termo é a regra de promoção [Termo inclusão] e esta
regra não nos ajuda nesta situação. Por isso temos de definir uma regra especial de introdução
do termo downcastσ[τ] em L4:
[Termo downcast]
Γ τ≤σ
Γ downcastσ[τ]:σ→τ
Esta regra é segura. Realmente, analisando os dois ramos da definição de downcastσ[τ],
verificamos que o primeiro ramo produz sempre valores do tipo τ (assumindo a correcta realização de checkType[τ]) e o segundo também produz sempre valores do tipo τ (pois trata-se do
termo divergeτ)
O facto de termos introduzido esta nova regra de tipo é mais um sinal de que realmente
estamos a ultrapassar as fronteiras de F+. Repare que L4 passou a ter um sistema de tipos independente, o qual requer argumentação independente relativamente à sua segurança.
4.3.5.4 Discussão
A introdução de tipos dinâmicos na linguagem L4, obrigou-nos a mudar a fundação da linguagem: F+ não permite a codificação directa dos novos mecanismos de L4.
O plano que propusemos para a definição da nova L4 foi o mais prático que conseguimos
imaginar, tendo em consideração o facto da linguagem L4 já estar codificada em F+. Eis um
resumo comentado do plano proposto:
• A generalidade das construções de L4 continuam a ser codificadas em F+ da mesma
forma, ou quase da mesma forma. É verdade que adicionámos a componente mytype a
todas as classes, mas essa é uma componente como outra qualquer, já suportada pela
semântica original, e além disso, ignorada pelas construções originais.
• As novas construções, checkType[τ] e downcastσ[τ], também são codificadas em F+ mas
passam a depender da funcionalidade da classe $DYNAMIC_TYPE. Digamos que a classe
$DYNAMIC_TYPE realiza a semântica da faceta dinâmica de L4.
• As construções class e class\c passam a ter a responsabilidade de introduzir a componente mytype correctamente inicializada.
• A boa definição semântica das construções checkType[τ] e downcastσ[τ] depende da correcção da classe $DYNAMIC_TYPE e da correcta inicialização da componente mytype em
cada classe.
4 Objectos simples com herança
63
• O sistema de tipos da nova versão de L4 seria automaticamente seguro se deixássemos
as restrições de F+ propagarem-se naturalmente a L4 pela via das codificações. Mas como o termo downcastσ[τ] é tipificado através duma regra especial, torna-se necessário
garantir separadamente a segurança desta regra.
A nossa linguagem final, OM, usa este método para introduzir tipos dinâmicos (cf. classes
$CoreObject e $DYNAMIC_TYPE, da secção 11.7).
[ACPP91, ACPR92] são dois importantes elementos da literatura sobre mecanismos de
tipificação dinâmica. Ambos os estudos trabalham o problema usando os métodos operacional
e denotacional directo. O segundo distingue-se por discutir tipos dinâmicos polimórficos.
4.3.5.5 Utilidade dos tipos dinâmicos
Terminamos com um breve comentário sobre a utilidade dos tipos dinâmicos. É importante
compreender que a utilidade dos tipos dinâmicos ultrapassa a aplicação que nos levou a discuti-los nesta secção, ou seja, o superar das deficiências do sistema de tipos.
Os tipos dinâmicos são essenciais quando um programa tem de lidar com tipos que não
podem ser determinados em tempo de compilação [ACPP91, ACPR92, Goo81, Hen92]. Em
particular, quando um programa recebe dados não homogéneos do exterior – de outro programa ou dum ficheiro – é conveniente que cada valor venha acompanhado por uma representação do seu tipo.
Os tipos dinâmicos são também úteis na escrita de funções polimórficas não-paramétricas.
Imagine, por exemplo, um procedimento print que tenha de lidar de forma não uniforme com
valores de vários tipos: esse procedimento deve poder fazer uma análise de casos sobre o tipo
do seu argumento.
4.3.6 Simulação em L4 do sistema de tipos do C++
A diferença mais notória entre os sistemas de tipos da linguagem L4 e da linguagem C++ reside na questão da separação dos conceitos de classe e de tipo-objecto: enquanto L4 separa estes
dois conceitos, o C++ unifica-os.
É instrutivo tentar simular em L4 a unificação dos conceitos de classe e de tipo-objecto.
Para isso temos de estabelecer uma bijecção entre classes e tipos-objecto que garanta que todos os objectos com um dado tipo são gerados por uma mesma classe e vice-versa. Conseguimos isso de forma simples, adicionando a cada classe uma componente auxiliar pública com
um nome único, não usado para nomear qualquer componente de qualquer outra classe. Se antes da inclusão destas componentes auxiliares, duas classes distintas de L4 podiam gerar tipos-objecto idênticos, depois daquela inclusão as duas classes passam a gerar tipos-objecto distintos. Repare que as componentes auxiliares se transferem das classes para os tipos-objecto por
elas gerados, onde actuam como elementos discriminantes.
64
OM – Uma linguagem de programação multiparadigma
Em C++, não são só as noções de tipo e classe que se misturam: as noções de subtipo e subclasse também se misturam. Nesta linguagem, não só “herança implica subtipificação” mas
também “subtipificação implica herança”. De facto, em C++, se A é subtipo de B então é porque estes tipos foram gerados a partir de classes que estão na relação de subclasse.
A nossa simulação, baseada em nomes únicos, verifica automaticamente esta última propriedade se, num programa, nos restrinjamos ao domínio dos tipos-objecto gerados pelas classes existentes. Note como as componentes artificiais são herdadas e se vão acumulando nas
subclasses à medida que se desce numa hierarquia de classes.
4.4 Conclusões
O sistema de tipos de L4 é bastante simples. Apesar disso, consegue capturar todos os aspectos
essenciais dos sistemas de tipos das linguagens C++ e Java, entre outras. Chega mesmo a ser
um pouco mais flexível na medida em que admite a mudança do tipo dos métodos redefinidos
nas subclasses, ao contrário do C++ e Java. Mas não esqueçamos que a simplicidade de L4
está associada a algumas fragilidades, partilhadas com as linguagens C++ e Java. Algumas
dessas fragilidades só encontrarão solução na próxima linguagem, L5.
No tratamento semântico dos aspectos dinâmicos de L4, baseámo-nos nos modelos denotacionais de herança de Kamin e Cook em [Red88, Kam88, CP89, KR93], os quais foram, por
sua vez, desenvolvidos a partir do modelo-dos-registos (record model) de Cardelli: historicamente, os modelos de Kamin e Cook foram os primeiros modelos definidos para linguagens do
estilo do Smalltalk. De qualquer forma, estes são modelos não-tipificados e nós apresentámos
um modelo tipificado para L4. Na tipificação do modelo seguimos a via, mais simples, de usar
apenas as construções de primeira ordem de F+. Obtivemos assim um sistema próximo do
C++, Java, etc., um sistema simples, mas que exibe deficiências que provocariam importantes
restrições de expressividade, não fora a existência de operações dinâmicas de teste e de despromoção de tipo.
Capítulo 5
Tipo SAMET, relações de
compatibilidade e de extensão
Sintaxe dos géneros,tipos e termos de L5
Κ ::= ∗ | ∗⇒Κ
τυϕϒI::= Bool | Nat | υ→τ | X |
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′ |
SAMET | CLASSTYPE(ϒ) | INTERFACE(ϒ) | OBJTYPE(ϒ)
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
self | super | class R | class\s R | new c | o.l |
checkType[τ] | downcastσ[τ]
Semântica dos tipos
INTERFACE(ϒ c) :∗⇒∗
ΛSAMET.ϒ c
OBJTYPE(ϒ c) :∗
=ˆ
=ˆ
interface
tipo-objecto
µSAMET.INTERFACE(ϒ c)[SAMET] (= ϒ c[OBJTYPE(ϒ c)/SAMET])
ˆ
CLASSTYPE(ϒ c) :∗ =
tipo-classe
∀ SAMET≤*INTERFACE(ϒ c).
SAMET→INTERFACE(ϒc)[SAMET]
Semântica das relações
Relação binária de compatibilidade
ˆ
X≤*INTERFACE(ϒ c) =
X≤INTERFACE(ϒ c)[X]
Relação binária de extensão entre interfaces
ˆ
INTERFACE(ϒ c) ext INTERFACE(ϒ s) =
*
*
T≤ INTERFACE(ϒ c) ⇒ T≤ INTERFACE(ϒ s)
Semântica dos termos
ˆ
class Rc :CLASSTYPE(ϒ c) =
*
λSAMET≤ INTERFACE(ϒ c).
λself:SAMET.
Rc
ˆ
class\s Rc :CLASSTYPE(ϒ s⊕ϒc) =
let S:CLASSTYPE(ϒ s) = s in
λSAMET≤*INTERFACE(ϒ s⊕ϒc).
λself:SAMET.
let super:INTERFACE(ϒ s)[SAMET] = (S[SAMET] self) in
super+Rc
*Restrição implícita: ϒ s,ϒ c devem ser tais que:
INTERFACE(ϒ s⊕ϒc) ext INTERFACE(ϒ s)
66
OM – Uma linguagem de programação multiparadigma
new c :OBJTYPE(ϒ c)
=ˆ
let C:CLASSTYPE(ϒ c) = c in
let gen:OBJTYPE(ϒ c)→OBJTYPE(ϒc) = C[OBJTYPE(ϒ c)] in
let o:OBJTYPE(ϒ c) = fix gen in
o
ˆ
o.l :τ =
let R:OBJTYPE(ϒ c) = o in
R.l
Na secção 5.1, começamos por introduzir informalmente os conceitos e mecanismos característicos da linguagem L5. Seguidamente, na secção 5.2 formalizamos a semântica de L5 e
introduzimos as importantes relação de compatibilidade e relação de extensão. Na secção 5.3,
enunciamos e demonstramos diversos teoremas relativos a estas relações. Na secção 5.4, introduzimos o operador “+”, um operador aplicável a classes que ajuda a conciliar o mecanismo
de herança de L5 com a relação de subtipo. Na secção 5.5, revisitamos a relação de extensão e
analisamos as técnicas de programação genérica aplicáveis em L5. Finalmente, na secção 5.6
tiramos algumas conclusões e relacionamos o nosso material com trabalhos de outros autores.
5.1 Conceitos e mecanismos de L5
Relativamente a L4, a linguagem L5 tem apenas uma modesta novidade sintáctica: o nome de
tipo SAMET passa a estar disponível dentro de todas as classes, onde representa o tipo de self .
Mas esta alteração não tem nada de insignificante a nível semântico, já que nos obriga a rever
os conceitos introduzidos em L4 e mesmo a mudar a forma prática de usar a linguagem.
Entre as linguagens com suporte para uma construção análoga a SAMET, contam-se as linguagens Trellis/Owl [SCB+86], Emerald [BHJL86], Eiffel [Mey88, Mey92], Sather [Omo92,
SO91], PolyTOIL [BSG95, BFSG98], LOOP [ESTZ94] e LOOM [BPF97]. Entre os estudos
teóricos que incluem construções semelhantes destacam-se [CHC90, Bru94, ESTZ94, BSG95,
BFSG98].
Tal como L4, a linguagem L5 é puramente funcional e os seus objectos só possuem parte
pública. Na linguagem L7, serão introduzidas variáveis de instância mutáveis e componentes
privadas nos objectos, mas isso não afectará as conclusões e teoremas no presente capítulo
relativamente à parte pública dos objectos de L7.
5.1.1 O tipo SAMET
Na linguagem L4, o tratamento do tipo de self é um incipiente, pois apesar de self estar sujeito a
reinterpretação nos métodos herdados, esses métodos são verificados sujeitos à condição de
que self tem um tipo fixo: exactamente o tipo-objecto correspondente à classe onde os métodos
são introduzidos.
5 Tipo SAMET, relações de compatibilidade e de extensão
67
A introdução do tipo SAMET nas classes representa uma nova atitude relativamente à tipificação de self em L5. Convencionalmente, o nome SAMET representa o tipo de self dentro de
cada classe individual.
Em L5, a reinterpretação de self nos métodos herdados continua a ser efectuada, mas agora
sendo acompanhada em paralelo pela reinterpretação de SAMET. A reinterpretação de SAMET
nas subclasses impõe novas regras de validação das classes. A partir de agora, na validação
das componentes duma classe c, será preciso considerar simultaneamente todos os significados
que SAMET possa assumir nas subclasses de c .
O tipo SAMET será, pois, tratado como um tipo aberto, i.e. parcialmente indeterminado, representando a família (infinita) dos tipos-objecto gerados por todas as subclasses potenciais de
c. Esta é uma opção de tipificação conservadora, mas segura, que reflecte a nossa continuada
adesão ao princípio da “reutilização sem reverificação”.
Note que o nome SAMET, já usado nos tipos-objecto de L4 com significado fixo (representava o próprio tipo-objecto), passa agora, também, a ser usado nas classes de L5 com significado aberto (a família dos tipos gerados por todas as extensões possíveis duma classe). Este uso
dual de SAMET é consistente, e também de grande conveniência do ponto de vista prático. É
consistente pois, no momento em que uma classe é usada para gerar um tipo-objecto, o carácter extensível da classe torna-se irrelevante, devendo então SAMET ser interpretado como o
tipo-objecto gerado por essa mesma classe: nesse momento preciso, os significados de SAMET
dentro da classe e dentro do respectivo tipo-objecto tornam-se equivalentes. É também conveniente pois, desta forma, todas as ocorrências de SAMET na interface duma classe transferem-se directamente para o tipo-objecto recursivo gerado a partir dessa classe: com efeito, observando uma classe conseguimos imaginar imediatamente qual o tipo-objecto gerado por essa
classe.
A introdução de SAMET em L5 permite uma tipificação mais precisa de self, o que flexibiliza o mecanismo de herança, aumentando as oportunidades de reutilização do código e minorando os problemas associados a essa reutilização: em particular os problemas da inflexibilidade na herança do tipo de self (cf. secção 4.3.3) e da definição de métodos binários (ver 4.3.4)
resolvem-se em L5 através dum uso criterioso de SAMET.
Para ilustrar a utilização de SAMET em L5, reformulamos agora o exemplo da secção 4.3:
Point2T
Point2I
point2PC
=ˆ OBJTYPE({x:Nat,y:Nat, sum:Nat,id:SAMET,inc:SAMET,eq:SAMET→Bool})
=ˆ INTERFACE({x:Nat,y:Nat, sum:Nat,id:SAMET,inc:Point2T,eq:SAMET→Bool})
=ˆ rec p2. λa:Nat.λb:Nat. class {
x=a, y=b,
sum=self.x+self.y,
id=self,
inc=new (p2 (succ self.x) (succ self.y)),
eq=λa:SAMET.(self.x=a.x & self.y=a.y)} }
68
OM – Uma linguagem de programação multiparadigma
Point3T
Point3I
point3PC
=ˆ OBJTYPE({x:Nat,y:Nat,z:Nat,sum:Nat,id:SAMET,inc:SAMET,eq:SAMET→Bool})
=ˆ INTERFACE({x:Nat,y:Nat,z:Nat, sum:Nat,id:SAMET,inc:Point3T,eq:SAMET→Bool})
=ˆ rec p3. λa:Nat.λb:Nat.λc:Nat. class\(point2PC a b) {
z=c,
sum=super.sum + self.z,
inc=new (p3 (succ self.x) (succ self.y) (succ self.z)),
eq=λa:SAMET.(super.eq a & self.z=a.z) }
As alterações desta nova versão localizam-se no tipo atribuído, nas interfaces, aos métodos
id e eq e ainda ao método binário eq. No caso dos métodos id e eq , anteriormente o seu tipo era
fixo, agora passou a ser SAMET. No caso do método binário eq, anteriormente não era possível
tipificar convenientemente este método, agora tal já é possível usando SAMET como o tipo do
argumento.
Note ainda que, na interface Point2I , não é possível atribuir o tipo SAMET ao método inc,
pois o sistema de tipos tem de se precaver quanto à possibilidade de inc ser herdado: se essa
herança ocorrer, é notório que o método inc, incluído na classe point2PC, continuará a produzir
objectos do tipo Point2T na subclasse, e não do tipo-objecto correspondente à subclasse.
5.1.2 Relação de extensão entre interfaces
Na linguagem L4, todas as subclasses duma classe geradora dum tipo-objecto τ estão submetidas à restrição de terem de gerar subtipos de τ . Este característica de L4 resulta da conjugação
de dois factores: da adesão da linguagem ao princípio da “reutilização sem reverificação”, e do
facto de só estarem disponíveis tipos fixos para serem usados na atribuição de tipos às componentes.
A linguagem L5 também adere ao princípio da “reutilização sem reverificação”. Contudo,
em L5 as componentes são verificados levando agora em conta a existência do tipo aberto
SAMET. Assim a relação binária de subclasse de L5 resulta mais rica do que a de L4, existindo
em L5 muitos casos de subclasses que não geram subtipos. Para dar um exemplo de subclasse
que não gera um subtipo consideremos, no contexto do exemplo anterior, as classes
(point3PC 0 0 0) e (point2PC 0 0): a primeira é subclasse da segunda mas o tipo-objecto gerado pela primeira – Point3T – não é subtipo do tipo-objecto gerado pela segunda – Point2T . Neste caso,
uma razão para a relação de subtipo falhar é o facto do método binário eq ter um argumento do
tipo SAMET: se Point3T fosse subtipo de Point2T então o tipo do argumento de eq evoluiria do
supertipo para o subtipo de forma covariante, o que entraria em conflito com a regra [Sub →] de
F+. (Note que SAMET representa Point2T dentro de Point2T , e que SAMET representa Point3T
dentro de Point3T).
Assim, em L5, “herança não implica subtipificação”. No entanto, como veremos na secção
5.2.2.2, o objectivo da “reutilização sem reverificação” vai também impor uma restrição sobre
todas as subclasses duma classe. Em L4, essa restrição expressa-se através duma relação binária, reflexiva e transitiva, definida sobre os tipos-objecto gerados pelas classes: a relação de
5 Tipo SAMET, relações de compatibilidade e de extensão
69
subtipo “≤”. No caso de L5, essa restrição vai expressar-se através duma relação binária, reflexiva e transitiva, definida agora sobre as interfaces das classes. Chamaremos relação de extensão a esta relação binária e usaremos o operador binário infixo “ext” para a representar. Assim,
em L5, é valida a seguinte regra: se duas classes estão na relação de subclasse então as respectivas interfaces estão na relação de extensão. Abreviadamente: em L5 “herança implica extensibilidade”.
Mas, apesar de herança não implicar subtipificação em L5, muitas subclasses em L5 continuarão a gerar subtipos. Isto poderá ser verificado, caso a caso, usando as regras de subtipos
de F+. Na prática, a ocorrência de métodos binários nas classes é a razão que ocorre mais frequente para a não geração de subtipos (cf. teoremas 5.3-11 e 5.3-12).
5.1.3 Tipificação aberta e tipificação fixa
Em L5, o tipo de self é SAMET. Esta tipificação está predefinida na linguagem, não estando ao
alcance do programador alterá-la. No entanto, relativamente a uma entidade qualquer, por
exemplo, um argumento dum método, que dentro duma classe deva ser declarada com o tipo
dos objectos gerados por essa mesma classe, o programador tem por vezes duas alternativas
razoáveis de tipificação: ou usa o tipo aberto SAMET, ou usa o tipo fixo associado aos objectos
gerados pela classe. No primeiro caso dizemos que o programador optou por uma tipificação
aberta da entidade; no segundo caso que optou por uma tipificação fixa da entidade.
Quando o programador usa sistematicamente tipificação aberta numa classe, ele manifestamente está a pensar um pouco para além da classe concreta que está a escrever, pois sabemos
que SAMET, na sua semi-indeterminação, representa não só o tipo-objecto associado à classe
corrente, mas também o tipo-objecto associado a qualquer uma das suas potenciais subclasses.
Esta escolha favorece o uso de herança e permite formas de reutilização de código que em L4
não eram possíveis. Mas surge agora um problema: dependendo das localizações exactas de
SAMET na interface da classe, as suas subclasses podem deixar de gerar subtipos úteis (de
acordo com a definição de subtipo inútil do teorema 5.3-12). Este aspecto é restritivo da liberdade do programador pois compromete a capacidade de programar genericamente: note que,
tal como em L4, a única forma de polimorfismo existente em L5 é o polimorfismo de inclusão,
uma forma de polimorfismo que depende da relação de subtipo. (Mais sobre este assunto na
secção 5.5.2).
Tudo se passa de forma inversa se o programador usar sistematicamente tipificação fixa
numa classe. Neste caso, o programador opta pela tipificação menos precisa e mais problemática de L4, aceitando ter de lidar com os problemas que ela acarreta, incluindo certas limitações de expressividade. Mas tem a vantagem de dispor duma classe cujas subclasses geram
subtipos, o que favorece a capacidade de escrever funções polimórficas.
Resumindo, a tipificação aberta favorece a generalidade e a eficácia do mecanismo de herança mas, em muitos casos, não propicia a geração de subtipos o que prejudica, ou impossibi-
70
OM – Uma linguagem de programação multiparadigma
lita mesmo, a capacidade de programação genérica. Por outro lado, a tipificação fixa prejudica
a herança mas favorece a criação de subtipos. Esta contradição mostra que, se queremos integrar estas duas formas de tipificação na linguagem L5, sem colocar o programador face a dilemas desmesurados, temos algum trabalho a efectuar.
Na discussão precedente considerámos apenas as duas situações extremas, em que o programador usa em cada classe apenas tipificação aberta, ou apenas tipificação fixa. No entanto
é possível misturar as duas formas de tipificação na mesma classe. É o que se passa no exemplo da secção 5.1.1: as classes Point2PC e Point3PC enfatizam o uso de tipificação aberta, mas
usam tipificação fixa no caso particular do método inc.
A introdução de SAMET em L5, ou seja a introdução de tipificação aberta, é um importante
avanço, mas prejudica, a capacidade de programar genericamente, como vimos. Um importante passo que permitiria proporcionar ao programador meios para minorar este problema,
consistiria em dotar a linguagem dum sistema de tipos que, sem prejudicar o princípio da
“reutilização sem reverificação”, permitisse rever nas subclasses algumas decisões de tipificação tomadas ao nível das superclasses. Especialmente importante seria a possibilidade de
definir, a partir duma classe a cujas subclasses não gerassem subtipos úteis, uma sua subclasse
imediata b, geradora do mesmo tipo-objecto que a, mas já garantindo a geração de subtipos
úteis por todas as suas subclasses. Isso seria feito fixando na subclasse b certas instâncias de
uso de tipificação aberta.
Apresentamos seguidamente um exemplo que concretiza este ideal para uma classe a cujas
subclasses não geram subtipos úteis, e uma sua subclasse b cujas subclasses já geram subtipos.
AT
a
b
=ˆ OBJTYPE({x:Nat,y:Nat,sum:Nat,eq:SAMET→Bool})
=ˆ class { x=a, y=b,
=ˆ
sum=self.x+self.y,
eq=λa:SAMET.(self.x=a.x & self.y=a.y) }
class\a { eq=λa:AT.(super.eq a) }
Note que o método eq foi redefinido na subclasse b, sendo agora o seu argumento alvo de tipificação fixa.
Um dos objectivos do sistema de tipos de L5 será a possibilidade de definir subclasses semelhantes à classe b deste exemplo. A relação de extensão será definida com suficiente generalidade para que a classe b seja considerada uma subclasse admissível de a .
5.2 Semântica de L5
Nesta secção, formalizamos e discutimos da semântica da linguagem L5, nomeadamente a
codificação dos tipos e dos termos da linguagem em F+. O essencial das formalizações aqui
descritas integra a tabela de equações que abre o presente capítulo.
5 Tipo SAMET, relações de compatibilidade e de extensão
71
5.2.1 Semântica dos tipos
Em L5, a forma geral duma classe sem superclasses é class Rc; a forma geral duma subclasse
imediata duma classe s é class\s Rc. Em ambos os casos usamos Rc para representar o registo das
componentes da classe. No que se segue assumiremos que o tipo de Rc é ϒ c.
Discutimos agora a codificação em F+ das interfaces de classe, dos tipos-objecto, e dos tipos-classe.
Representa a interface duma classe com componentes ϒc. Sintacticamente,
o que há de novo nas classes de L5 relativamente a L4 é a introdução do nome SAMET no contexto das classes. Respeitando a característica essencial das interfaces, não fazemos qualquer
interpretação prematura de SAMET e formalizamos a interface INTERFACE(ϒc) usando o operador de tipo ΛSAMET.ϒc. Nesta formalização, a única assunção que efectivamente se faz relativamente a SAMET é que denota um tipo.
INTERFACE(ϒc):
OBJTYPE(ϒ c):
Representa o tipo-objecto gerado por uma classe com interface
INTERFACE(ϒ c). É definido como µSAMET.INTERFACE(ϒc)[SAMET]. Note que o nome SAMET,
que no contexto duma interface estava por interpretar, no contexto dum tipo-objecto é interpretado como sendo uma referência recursiva ao próprio tipo-objecto OBJTYPE(ϒc).
CLASSTYPE(ϒc):
Representa o tipo-classe das classes com interface INTERFACE(ϒc). A parte
mais delicada da formalização dum tipo-classe reside na forma de tratar o nome SAMET dentro
duma classe. Em CLASSTYPE(ϒ c), o nome SAMET é introduzido como um tipo-objecto genérico
sujeito à restrição SAMET≤INTERFACE(ϒc)[SAMET]. Como veremos adiante, sob esta condição
SAMET representa, genericamente, todos os tipos-objecto gerados pelas subclasses duma classe
c com tipo CLASSTYPE(ϒc). O tipo-classe CLASSTYPE(ϒc) é formalizado usando o tipo:
∀ SAMET≤INTERFACE(ϒc)[SAMET].
SAMET→INTERFACE(ϒc)[SAMET].
O resto desta secção é dedicado a discutir os detalhes da formalização de CLASSTYPE(ϒc).
Vamos começar por motivar a condição que caracteriza todas as interpretações possíveis de
SAMET nas subclasses. Como primeiro passo, consideramos apenas subclasses x, duma classe
c, que não redefinam os métodos herdados de c nem introduzam novos métodos relativamente
a c. Neste caso, o tipo SAMET dos objectos gerados por x contém exactamente as componentes
que ocorrem na interface de c , pelo que simbolicamente podemos escrever: SAMET≡
INTERFACE(ϒ c)[???]. Falta apenas ver qual é a instanciação correcta da interface: faremos
???≡SAMET pois, como foi convencionado, a interpretação do parâmetro da interface dentro da
classe x é o tipo dos objectos gerados por x, ou seja o próprio tipo que estamos correntemente a
tentar definir. Portanto, em qualquer subclasse x de c , que não altere nada relativamente a c,
verifica-se a equivalência SAMET≡INTERFACE(ϒc)[SAMET].
Temos agora de levar em conta os casos restantes, aqueles em que a subclasse x redefine
métodos herdados de c ou introduz novos métodos. Para isso enfraquecemos um pouco a equi-
72
OM – Uma linguagem de programação multiparadigma
valência anterior. A escolha da condição SAMET≤INTERFACE(ϒc)[SAMET] é razoável pois é
compatível com a adição dum número irrestrito de novos métodos e admite alguma liberdade
na mudança do tipo dos métodos herdados quando são redefinidos. Todos os tipos SAMET que
verificam esta condição têm a estrutura recursiva básica de µSAMET.INTERFACE(ϒc)[SAMET] e,
ainda, possivelmente, métodos adicionados e métodos redefinidos.
Encontrámos pois uma caracterização do significado de SAMET nas classes. Vamos agora
motivar o tipo-classe: CLASSTYPE(ϒc). Este tipo tem de ser parametrizado em função de todos
os significados admissíveis para SAMET nas subclasses. Tem, além disso, de depender do tipo
de self porque todas as classes serão parametrizadas em função do significado de self, pelas
mesmas razões de L4. Assim o esquema geral da codificação de CLASSTYPE(ϒc) será:
∀ SAMET≤INTERFACE(ϒc)[SAMET].
SAMET→(tipo dos resultados)
Por causa da herança, é prematuro usar o tipo OBJTYPE(ϒc) como tipo dos resultados, não
obstante ser exactamente este o tipo dos objectos gerados por uma classe do tipo
CLASSTYPE(ϒ c). O tipo dos resultados terá de depender do tipo aberto SAMET para que a reinterpretação de SAMET possa ter lugar no tipo dos métodos herdados, nos objectos gerados
pelas subclasses; i.e. temos de nos preocupar não só com o tipo dos objectos gerados pela
classe corrente, mas também com o tipo dos objectos gerados pelas suas subclasses. Assim o
tipo escolhido para os resultados é INTERFACE(ϒc)[SAMET], com SAMET quantificado da forma
anteriormente apresentada. Pensando no caso particular dos objectos gerados pela classe corrente, i.e. tomando SAMET≡OBJTYPE(ϒc), obtemos
INTERFACE(ϒ c)[SAMET] = INTERFACE(ϒc)[OBJTYPE(ϒc)] = OBJTYPE(ϒ c)
o que ajuda a confirmar a justeza da escolha efectuada.
Juntando os vários elementos, concluímos que o tipo-classe CLASSTYPE(ϒc) deve ser codificado usando o tipo universal F-restringido:
∀ SAMET≤INTERFACE(ϒc)[SAMET].
SAMET→INTERFACE(ϒc)[SAMET]
Definição 5.2.1-1 (Relação de compatibilidade) Chamamos relação de compatibilidade, ≤* , à relação binária entre um tipo-objecto X e uma interface INTERFACE(ϒc) que se define, por tradução para F+, da seguinte forma:
X≤ * INTERFACE(ϒ c)
=ˆ X≤INTERFACE(ϒc)[X]
Nota: A asserção X≤ * INTERFACE(ϒ c) lê-se assim: “O tipo-objecto X é compatível com a interface INTERFACE(ϒc)”.
Usando esta relação de compatibilidade, a formalização de CLASSTYPE(ϒc) pode ser reescrita da seguinte forma mais compacta:
5 Tipo SAMET, relações de compatibilidade e de extensão
73
∀ SAMET≤ * INTERFACE(ϒ c).
SAMET→INTERFACE(ϒc)[SAMET]
Podemos agora dizer que as reinterpretações admissíveis de SAMET nas subclasses duma classe c são os tipos compatíveis com a interface de c.
5.2.2 Semântica dos termos
Nesta secção, analisamos a semântica das classes e subclasses de L5, assim como a semântica
das operações de criação de objectos e de acesso a componentes desses objectos. As respectivas equações semânticas encontram-se na tabela que abre o presente capítulo.
5.2.2.1 Semântica das classes
Na equação que define a classe class Rc, o gerador polimórfico que aí é introduzido está parametrizado relativamente a SAMET e a self . Além disso, ele gera objectos constituídos por todas
as componentes declaradas na classe. Note que a definição faz implicitamente a validação do
código da classe: se o registo Rc estiver mal tipificado então a classe class Rc também está mal
tipificada.
Formalizamos o tratamento da herança na equação do termo subclasse imediata class\s R c.
Para chegarmos a essa formalização, o primeiro passo consiste na determinação da interface da
nova subclasse. Essa interface é INTERFACE(ϒ s ⊕ϒc): resulta portanto da combinação, usando o
operador ⊕, dos tipo-registo das componentes herdadas, ϒs , com o tipo-registo das componentes definidas na subclasse ϒc. O tipo-objecto gerado pela subclasse é OBJTYPE(ϒs ⊕ϒc). Fazemos
as reinterpretações de SAMET e self na subclasse da seguinte forma. Primeiro introduzimos uma
nova variável de tipo SAMET compatível com a interface da subclasse e um novo self do tipo
SAMET. Seguidamente, aplicamos o gerador S correspondente à superclasse a estes novos
SAMET e self (e chamamos super ao resultado): desta forma ajustamos as componentes da superclasse ao contexto da subclasse. Finalmente, enriquecemos o conjunto das componentes herdadas usando o operador +. Assim criámos um novo gerador por extensão dum gerador existente.
Em L5, tal como em L4, o nome super encontra-se definido no contexto de toda a subclasse.
A definição original de super em L4 é generalizada, ficando agora super ligado a (S[SAMET] self).
Note que, nos geradores de L5, enquanto self constitui um nome não ligado (por ser um parâmetro), super já um nome ligado (ligado a (S[SAMET] self)).
5.2.2.2 Boa formação das subclasses
Sem abandonar ainda o tratamento da herança, consideremos agora a questão da boa tipificação do termo de F + que codifica a subclasse class\s Rc. Para que este termo esteja bem tipificado
é necessário, em primeiro lugar, que o registo super+Rc esteja bem tipificado. Uma outra questão, mais subtil, prende-se com a boa tipificação do termo que adapta as componentes da
superclasse ao contexto da subclasse:
74
OM – Uma linguagem de programação multiparadigma
super = S[SAMET] self
Neste termo, o gerador polimórfico S, correspondente à superclasse, espera um tipo-objecto
SAMET que obedeça à condição SAMET≤ * INTERFACE(ϒ s ) mas é aplicado, em (S[SAMET] self), a
um tipo-objecto SAMET relativamente ao qual apenas se sabe que SAMET≤ * INTERFACE(ϒ s ⊕ϒc).
Para que o termo esteja bem tipificado é pois necessário que os tipos-registo ϒs , ϒ c sejam tais,
que a seguinte condição se verifique:
SAMET≤* INTERFACE(ϒ s ⊕ϒc) ⇒ SAMET≤ * INTERFACE(ϒ s )
Esta é a condição mais geral que se consegue encontrar e que garante a boa tipificação do termo (S[SAMET] self). Ela estabelece que qualquer tipo-objecto compatível com a interface da
subclasse será também compatível com a interface da superclasse.
Neste ponto, é tentador investigar a possibilidade de se usar uma relação mais simples mas
que, apesar de tudo, seja suficientemente forte para garantir a boa tipificação do termo
(S[SAMET] self). Uma relação mais simples seria menos complicada de verificar automaticamente pelo compilador, e, acima de tudo, menos confusa de usar na prática pelo programador.
A hipótese mais óbvia, neste sentido, é a seguinte condição, baseada na relação de subtipo
entre operadores de tipo:
INTERFACE(ϒ s ⊕ϒc)≤INTERFACE(ϒs )
Contudo esta condição tem o grave inconveniente de bloquear nas subclasses todas as decisões
de tipificação, aberta ou fixa, tomadas numa classe relativamente a SAMET (cf. 5.1.3); este bloqueio é inconveniente porque empobrece a relação de subtipo em muitos programas, e esta
relação faz-nos falta (cf. secção 5.5.2 e [CHC90]).
Assim, por agora, manteremos a condição geral que deduzimos inicialmente. Voltaremos à
questão da sua simplificação na secção 5.5.1.
A condição geral que garante a boa tipificação do termo (S[SAMET] self) induz uma relação
binária no conjunto das interfaces das classes. Chamaremos essa relação de relação de extensão, pois ela estabelece quais são as extensões de interface válidas que dão origem a subclasses bem formadas. É uma relação reflexiva e transitiva (cf. teorema 5.3-7), o que é essencial
para que a relação de subclasse continue a ser reflexiva e transitiva.
Definição 5.2.2.2-1 (Relação de extensão) Chamamos relação de extensão, ext, à relação binária entre interfaces que se define, por tradução para F+, da seguinte forma:
INTERFACE(ϒ c) ext INTERFACE(ϒ s ) =ˆ
T≤ * INTERFACE(ϒ c) ⇒ T≤ * INTERFACE(ϒ s )
Sinteticamente, podemos dizer que “herança implica extensibilidade” em L5 pois sem a
verificação da restrição de extensibilidade entre interfaces não seria possível ter herança no
modelo. A implicação contrária não se verifica, em geral, pois duas interfaces podem estar na
5 Tipo SAMET, relações de compatibilidade e de extensão
75
relação de extensão sem que as classes donde elas foram extraídos estejam na relação de subclasse: o facto das classes terem interfaces relacionadas poderá ser simples coincidência.
Note que, tal como em L4, conseguimos escrever a equação relativa ao termo class\s Rc sem
explicitar as componentes da superclasse; foi apenas necessário explicitar o tipo das componentes da superclasse. Por isso o mecanismo de herança de L5 é também compatível com a
compilação separada de classes, isto é, com o principio da “reutilização sem reverificação“.
5.2.2.3 Semântica dos outros termos
Há ainda duas construções de L5 por explicar.
Na equação semântica referente à criação de objectos – termo new c – começamos por
instanciar o gerador polimórfico correspondente à classe c com o tipo dos objectos a criar:
vemos assim que só no momento da criação dum objecto o nome SAMET é realmente ligado.
Desta instanciação obtemos um gerador monomórfico C do tipo OBJTYPE(ϒc)→OBJTYPE(ϒc).
Este gerador é semelhante aos geradores de L4, e a ele aplicamos o operador de ponto fixo
para estabelecermos a ligação do nome self no contexto do novo objecto.
Quando à definição do termo que descreve o acesso às componentes dos objectos – termo
o.l – nada muda relativamente a L4. Codificamos este termo por meio dum simples acesso a
uma componente dum registo.
5.2.3 Relação de subtipo vs. relação de compatibilidade
Consideremos uma classe a com interface AI e geradora do tipo-objecto AT. Consideremos
também uma subclasse qualquer b de a, com interface BI e geradora do tipo-objecto BT: b pode
ser subclasse imediata ou subclasse não-imediata de a.
Sabemos que na linguagem L4 “herança implica subtipificação”. Por isso, sendo b subclasse de a, em L4 podemos garantir que BT≤AT. Já na linguagem L5 não se pode garantir esta
propriedade, não obstante ela poder ser verdadeira muitas vezes (ver, por exemplo, o teorema
5.3-11).
Saber que BT≤AT é útil de duas formas, tanto em L4 como em L5. Em primeiro lugar, ganhamos a liberdade de usar expressões do tipo BT onde se esperam expressões do tipo AT: esta
propriedade designa-se por substitutividade. Em segundo lugar ficamos a saber que os objectos de BT sabem responder a todas as mensagens a que os objectos de AT sabem responder (cf.
regra [Sub {…}]). Esta segunda propriedade é uma consequência da primeira, mas interessa-nos
considerá-la separadamente aqui.
Agora, apenas no contexto da linguagem L5, e continuando a considerar as classes a e b, assumamos que BT/≤AT. Neste caso não podemos relacionar os tipos-objecto BT e AT directamente. O máximo que podemos dizer sobre BT é que este tipo é compatível com a interface AI da
classe a, ou seja BT≤ * AI , ou ainda BT≤AI[BT]. Mas saber que BT≤AI[BT] não é tão útil como sa-
76
OM – Uma linguagem de programação multiparadigma
ber que BT≤AT . Efectivamente, com base apenas na condição BT≤AI[BT], não se pode garantir
que AT é substituível por BT. Apesar de tudo, com base na condição BT≤AI[BT] já se pode garantir que o tipo BT suporta todas as mensagens registadas no tipo AI[BT].
Dentro duma classe a com interface AI e geradora do tipo-objecto AT , a variável de tipo
SAMET é introduzida subordinada à restrição SAMET≤AI[SAMET]. Da discussão anterior resulta
que, no contexto da classe a, tudo o que se pode garantir relativamente aos objectos do tipo
SAMET, é que eles sabem responder às mensagens previstas em AI[SAMET].
5.3 Propriedades das relações de
compatibilidade e extensão
Ao longo desta extensa secção, apresentamos uma sequência de lemas e teoremas que nos ajudam a compreender melhor as duas relações binárias introduzidas em L5. Quase todos os
resultados que mostramos têm interesse prático. Mas são particularmente importantes os teoremas 5.3-11 e 5.3-13 que mostram como conciliar a utilização de SAMET nas classes com a
geração de subtipos pelas subclasses.
Começamos por sumariar os vários lemas e teoremas fazendo o seu enquadramento.
Os lemas que apresentamos no início ensinam-nos a tirar partido do uso de nomes simbólicos para simplificar o tratamento da recursão. De entre estes, os lemas 4 e 5 são particularmente úteis pois ensinam-nos a visualizar rápida se um tipo-objecto é subtipo de outro tipo-objecto, ou se um tipo-objecto é compatível com uma interface.
O primeiro teorema da lista, 5.3-7, mostra que a relação ext é reflexiva e transitiva.
As relações de subtipo e compatibilidade não são directamente comparáveis por terem domínios diferentes. No entanto, uma interface determina univocamente um tipo-objecto pelo
que faz sentido comparar as duas relações de forma indirecta. Neste sentido, o teorema 5.3-8
mostra que a relação de compatibilidade não é nem mais forte nem mais fraca do que a relação
de subtipo.
A relação de extensão é complexa, sendo por vezes difícil verificar se um par de interfaces
concretas pertencem à relação. Os teoremas 5.3-9 e 5.3-10 delimitam relação de extensão entre
duas relações mais simples e mais fáceis de verificar. Concretamente, o teorema 5.3-9 mostra
que, comparando indirectamente as relações, a relação de compatibilidade é mais fraca do que
a relação de extensão. Por seu turno, o teorema 5.3-10 mostra que, comparando directamente
as relações, a relação de extensão é mais fraca do que a relação de subtipo entre operadores de
tipo.
O teorema 5.3-11 estabelece que se na interface duma classe todas as ocorrências de SAMET
forem positivas, então todas as suas subclasses geram subtipos. É complementado pelo teorema 5.3-12 que mostra que se a interface da classe contiver ocorrências negativas de SAMET
5 Tipo SAMET, relações de compatibilidade e de extensão
77
(basta uma) então as suas subclasses não geram subtipos, exceptuando alguns casos marginais,
de pouco interesse prático em que os subtipos gerados serão por nós designados por subtipos
inúteis.
Repare que afirmar que na interface duma classe todas as ocorrências de SAMET são positivas, é equivalente a afirmar que essa classe não contém métodos binários. Por sua vez, dizer
que a interface da classe contém ocorrências negativas de SAMET, equivale a dizer que a classe
contém métodos binários.
O importante teorema 5.3-13 ensina-nos como, a partir duma classe a com ocorrências negativas de SAMET na sua interface, se consegue criar uma classe b quase idêntica, e gerando o
mesmo tipo-objecto, mas cujas subclasses já geram subtipos.
O teorema 5.3-14 mostra que o teorema 5.3-13 não se estende às ocorrências positivas de
SAMET. Aliás, basta aplicar a substituição descrita no teorema 5.3-13 a uma única ocorrência
positiva de SAMET para que a classe resultante não não seja subclasse da primeira classe. De
qualquer forma este problema é benigno pois, de acordo com o teorema 5.3-11, as ocorrências
positivas de SAMET não impedem a geração de subtipos pelas subclasses.
Os teoremas 5.3-15 e 5.3-16 são menos importantes do ponto de vista prático, mas ajudam-nos a completar o conhecimento da relação de extensão. O primeiro ensina-nos que partindo
duma classe a onde todas as ocorrências de SAMET são positivas, é possível criar uma nova
subclasse b de a, obtida a partir de a substituindo no tipo dos métodos herdados uma ou mais
ocorrências positivas do tipo gerado por a por SAMET; dizemos, neste caso, que se abre uma
ocorrência positiva do tipo (fixo) gerado por a. O segundo mostra que o teorema 5.3-15 não é
extensível às ocorrências positivas de SAMET. Aliás, basta aplicar a substituição descrita no
teorema 5.3-15 a uma única ocorrência negativa de SAMET para que a classe resultante deixe
de ser subclasse da primeira.
Lema 5.3-1 Se AT=OBJTYPE(ϒA) então:
AT=ϒA[AT/SAMET]
Prova:
AT
= OBJTYPE(ϒA)
= µSAMET.INTERFACE(ϒA)[SAMET]
= µSAMET.(ΛSAMET.ϒA)[SAMET]
= µSAMET.ϒA[SAMET]
= ϒA[AT/SAMET]
por hipótese
por definição de OBJTYPE
por definição de INTERFACE
pela regra [Tipo= β Λ] de F+
pela regra [Tipo= fold/unfold µ]
Lema 5.3-2 Se AI=INTERFACE(ϒ A) e BT=OBJTYPE(ϒB) então:
AI[BT]=ϒA[BT/SAMET]
78
OM – Uma linguagem de programação multiparadigma
Prova:
AI[BT]
= (Λ SAMET.ϒA)[BT]
= ϒA[BT/SAMET]
por definição de interface
pela regra [Tipo= β Λ]
Lema 5.3-3 O tipo-objecto gerado por uma interface é compatível com essa interface. Formalmente, se AI=INTERFACE(ϒA) e AT=OBJTYPE(ϒA) então:
AT≤ * AI
Prova:
AT
= ϒA[AT/SAMET]
= AI[AT]
pelo lema 5.3-1
pelo lema 5.3-2
Usando a regra [Sub =] temos:
AT≤AI[AT]
donde por definição de ≤ * :
AT≤ * AI[AT]
Lema 5.3-4 Se AT=OBJTYPE(ϒA) e BT=OBJTYPE(ϒB) então:
BT≤AT ⇔ ϒB[BT/SAMET]≤ϒA[AT/SAMET]
Nota: Este lema fornece uma regra prática para verificar se o tipo-objecto BT é subtipo do
tipo-objecto AT: basta ver se o tipo-registo que integra BT é subtipo do tipo-registo que integra
AT, depois de se ter substituído SAMET por BT em BT e SAMET por AT em AT.
Prova: Imediato, aplicando o lema 5.3-1 a ambos os lados do operador ≤ .
Lema 5.3-5 Se AI=INTERFACE(ϒ A) e BT=OBJTYPE(ϒB) então
BT≤* AI ⇔ ϒB[BT/SAMET]≤ϒA[BT/SAMET]
Nota: Este lema fornece uma regra prática para verificar se o tipo-objecto BT é compatível
com a interface AI : basta ver se o tipo-registo que integra BT é subtipo do tipo-registo que integra AI , depois de se ter substituído SAMET por BT nos dois tipos-registo.
Prova: Pelo lema 5.3-1 temos:
BT=ϒ B[BT/SAMET]
Pelo lema 5.3-2 temos:
AI[BT]=ϒA[BT/SAMET]
5 Tipo SAMET, relações de compatibilidade e de extensão
79
Pela definição da relação de compatibilidade temos:
BT≤* AI ⇔ BT≤AI[BT]
Por substituição, o resultado sai imediatamente
Lema 5.3-6 Sejam AI =ˆ INTERFACE(ϒ A), BI =ˆ INTERFACE(ϒ B). Então a relação ext cuja definição original é:
AI ext BI
=ˆ (T≤AI[T] ⇒ T≤BI[T])
tem a seguinte definição alternativa:
AI ext BI
=ˆ (T≤AI[T] ⇒ AI[T]≤BI[T])
Nota: Este resultado é o elemento chave da demonstração dos teoremas 7.2.2.2-3 e 9.2.2.3-1.
Prova: Vamos desenvolver a prova usando uma versão equivalente das regras de subtipo de F +
(cf. secção 2.3.4.2) na qual a regra [Sub var-trans2] é usada em lugar da regra original [Sub trans]
(cf. secção 2.3.4.5). Considerando as condições T≤AI[T]⇒T≤BI[T] e T≤AI[T]⇒AI[T]≤BI[T], o nosso objectivo é mostrar que elas são equivalentes.
Se AI=BI, então as duas condições degeneram em tautologias, e duas tautologias são sempre
equivalentes.
Assumindo agora AI≠BI, vamos estudar a estrutura das provas que permitem provar a implicação T≤AI[T]⇒T≤BI[T]. Para isso escrevemos esta condição sob a forma dum juízo:
∅‚T≤AI[T] T≤BI[T]
Qualquer prova deste juízo, a existir, terá naturalmente a forma:
…
∅‚T≤AI[T] T≤BI[T]
Examinando as regras que definem a relação de subtipo, verificamos que as regras [Sub κµ],
[Sub X], [Sub refl] e [Sub var-trans2] são as únicas que permitem deduzir uma conclusão da forma
(… T≤…), onde T é uma variável de tipo. Mas a primeira regra não é aplicável porque BI[T] não
é um tipo recursivo; a segunda também não porque T≤BI não ocorre no contexto ∅‚T≤AI[T]; a
terceira também não porque o nosso juízo não tem a forma … T≤T. Assim resta a regra
[Sub var-trans2], e logo a nossa prova terá necessariamente a forma:
∅‚T≤AI[T] AI[T]≤BI[T] ∅‚T≤AI[T] AI[T]:K
∅‚T≤AI[T] T≤BI[T]
Mas sendo esta a única prova para o juízo ∅‚T≤AI[T] T≤BI[T], concluímos que a validade
deste juízo é equivalente à validade do juízo ∅‚T≤AI[T] AI[T]≤BI[T] (assumindo que os tipos
envolvidos estão bem formados). Era isto o que queríamos provar.
Teorema 5.3-7 A relação de extensão ext é reflexiva e transitiva.
80
OM – Uma linguagem de programação multiparadigma
Prova: Sejam AI =ˆ INTERFACE(ϒ A), BI =ˆ INTERFACE(ϒ B), CI =ˆ INTERFACE(ϒ C).
A relação ext é reflexiva pois a asserção:
AI ext AI
é equivalente à tautologia:
T≤AI[T] ⇒ T≤AI[T]
Para verificarmos a transitividade vamos assumir:
AI ext BI
BI ext CI
ou seja:
T≤AI[T] ⇒ T≤BI[T]
T≤BI[T] ⇒ T≤CI[T]
Por transitividade de ⇒ obtemos
T≤AI[T] ⇒ T≤CI[T]
ou seja:
AI ext CI
Teorema 5.3-8 Sejam
AI =ˆ INTERFACE(ϒ A), AT =ˆ OBJTYPE(ϒA),
e T um tipo-objecto qual-
quer. Então:
T≤ * AI ⇒
/ T≤AT
T≤AT ⇒
/ T≤* AI
Prova: Basta apresentar um contra-exemplo adequado a cada uma das asserções.
Contra-exemplo para a primeira asserção:
AT
AI
T
=ˆ OBJTYPE({x:Nat, eq:SAMET→Bool})
=ˆ INTERFACE({x:Nat, eq:SAMET→Bool})
=ˆ OBJTYPE({x:Nat, y:Nat, eq:SAMET→Bool})
Usando o lema 5.3-5 é imediato que T≤ * AI . No entanto não é verdade que T≤AT pois, pelo
lema 5.3-4, a asserção T≤AT é equivalente a
{x:Nat,y:Nat,eq:T→Bool}≤{x:Nat,eq:AT→Bool}
a qual implica T→Bool≤AT→Bool e seguidamente AT≤T, o que não é possível pois AT tem menos componentes que CT.
Contra-exemplo para a segunda asserção:
5 Tipo SAMET, relações de compatibilidade e de extensão
AT
AI
T
81
=ˆ OBJTYPE({x:Nat, f:Nat→SAMET})
=ˆ INTERFACE({x:Nat, f:Nat→SAMET})
=ˆ OBJTYPE({x:Nat, y:Nat, f:Nat→AT})
É verdade que T≤AT , pois pelo lema 5.3-4 esta asserção é equivalente à condição verdadeira:
{x:Nat,y:Nat,f:Nat→AT}≤{x:Nat,f:Nat→AT}
Mas não temos T≤* AT, pois pelo lema 5.3-5 esta asserção é equivalente à condição:
{x:Nat,y:Nat,f:Nat→AT}≤{x:Nat,f:Nat→T}
a qual implica Nat→AT≤Nat→T e seguidamente AT≤T (cf. regra [Sub →]), o que não é possível
pois AT tem menos componentes que CT (cf. regra [Sub {…}]).
Teorema 5.3-9 Se AI=INTERFACE(ϒA), BI=INTERFACE(ϒB) e BT=OBJTYPE(ϒB) então:
BI ext AI ⇒ BT≤* AI
BT≤* AI ⇒
/ BI ext AI
ou seja, se a interface BI estende a interface AI , então o tipo BT, gerado pela primeira interface,
é compatível com a segunda interface. A implicação recíproca não é verdadeira.
Nota: A primeira asserção deste teorema fornece uma regra prática para verificar de forma expedita se uma interface pode ser uma extensão de outra interface (ou seja, se uma classe pode
ser subclasse de outra classe). Recordamos que o lema 5.3-5 permite verificar BT≤ * AI de forma
simples.
Prova: Para provar a primeira asserção, assumamos a hipótese:
BI ext AI
ou seja:
T≤ * BI ⇒ T≤* AI
Pelo lema 5.3-3 sabemos que:
BT≤* BI
donde, usando a hipótese, se obtém imediatamente:
BT≤* AI
o que prova a primeira asserção.
Para provar a segunda asserção, consideremos o seguinte contra-exemplo:
82
OM – Uma linguagem de programação multiparadigma
AT
AI
BT
BI
CT
=ˆ
=ˆ
=ˆ
=ˆ
=ˆ
OBJTYPE({x:Nat, eq:SAMET→Bool})
INTERFACE({x:Nat, eq:AT→Bool})
OBJTYPE({x:Nat, eq:SAMET→Bool})
INTERFACE({x:Nat, eq:SAMET→Bool})
OBJTYPE({x:Nat, y:Nat, eq:SAMET→Bool})
Pelo lema 5.3-3, e porque AT=BT, temos
BT≤* AI
No entanto não é verdade que BI ext AI pois existe um tipo CT, tal que CT≤* BI (fácil de verificar usando o lema 5.3-5) mas não CT≤* AI . Note que esta última asserção é, pelo lema 5.3-5,
equivalente a
{x:Nat,y:Nat,eq:CT→Bool}≤{x:Nat,eq:AT→Bool}
a qual nunca poderia ser verdadeira, pois para isso teríamos de ter CT→Bool≤AT→Bool e logo
AT≤CT. Mas isso não é possível pois AT tem menos componentes que CT.
Teorema 5.3-10 A relação de subtipo entre interfaces (cf. regra [Sub Λ ]) é mais forte do que
a relação de extensão. Formalmente, tomando AI =ˆ INTERFACE(ϒ A) e BI =ˆ INTERFACE(ϒ B) temos:
BI≤AI ⇒ BI ext AI
Nota: Este teorema fornece uma regra prática que, em muitos casos, permite verificar de forma
expedita que uma interface é uma extensão de outra interface. Note que de acordo com a regra
[Sub Λ ], para determinarmos se o operador de tipo ϕ =ˆ ΛSAMET.τ é subtipo do operador de tipo
ϕ′ =ˆ ΛSAMET.τ′, basta verificar τ≤τ′ assumindo que SAMET é apenas subtipo de si próprio (ou
ainda, do tipo-objecto vazio OBJTYPE({})). Esta relação é simples, mas se fosse usada para fundar o mecanismo de herança, as decisões de tipificação, aberta ou fechada, tomadas ao nível
duma classe, nunca poderiam ser revistas ao nível das subclasses.
Prova: Com o objectivo de concluirmos no final que T≤* AI , consideremos um tipo-objecto T
tal que T≤ * BI, ou seja T≤BI[T]. Tomando o antecedente, BI≤AI, da implicação que pretendemos
demonstrar e aplicando os operadores de tipo BI e AI a T obtemos BI[T]≤AI[T] (cf. regra
[Sub aplic Λ ]). Usando agora T≤BI[T] e a transitividade de ≤ obtemos T≤AI[T] , ou seja T≤* AI
como pretendíamos.
Teorema 5.3-11 Seja
AI+ =ˆ INTERFACE(ϒ A),
uma interface onde todas as ocorrências de
SAMET são positivas. Seja AT =ˆ OBJTYPE(ϒA) o tipo-objecto correspondente. Então, dada uma
interface qualquer BI =ˆ INTERFACE(ϒ B) e correspondente tipo-objecto BT =ˆ OBJTYPE(ϒB) verifica-se:
BT≤* AI + ⇒ BT≤AT
BI ext AI+ ⇒ BT≤AT
5 Tipo SAMET, relações de compatibilidade e de extensão
83
Nota: A segunda asserção tem um grande interesse prático pois indica que “herança implica
subtipificação” nos casos em que todas as ocorrências de SAMET na interface da superclasse
são positivas.
Prova: Dizer que todas as ocorrências de SAMET em AI+ são positivas é equivalente a dizer
que a interface AI+ é um operador de tipo crescente. Desta forma:
BT≤* AI +
⇔ BT≤AI +[BT]
⇒ BT≤AT
por definição de ≤ *
pela regra [Sub κµ] de F+
A segunda asserção resulta da combinação da primeira asserção com a primeira parte do
teorema 5.3-9.
Teorema 5.3-12 Seja AI uma interface contendo ocorrências negativas de SAMET, e seja AT
o tipo-objecto correspondente. Seja T um tipo-objecto qualquer. Então:
T≤AT ⇒ Todas as ocorrências negativas de SAMET em AI foram substituídas em T por supertipos de AT
Nota: Este resultado mostra que, dada uma classe com ocorrências negativas de SAMET, é difícil encontrar, entre as suas subclasses que têm a particularidade de gerar subtipos, alguma com
utilidade prática (note que a substituição, em AI, de SAMET por um supertipo de AT, corresponde a perda de informação). Por esse motivo chamamos, genericamente, a essas subclasses,
subclasses inúteis, e aos subtipos-objecto respectivos, subtipos inúteis. Provavelmente, de
entre as subclasses consideradas, a única com utilidade prática é aquela em que todas as ocorrências negativas de SAMET são substituídas simplesmente por AT (cf. teorema 5.3-13).
Prova: Começamos por analisar um caso particular, que corresponde à situação mais frequente. Consideremos a interface, e respectivo tipo-objecto:
AI
AT
=ˆ ΛSAMET.{f:SAMET→Z}
=ˆ {f:AT→Z}
Sendo T≤AT, então T tem necessariamente a forma:
T ≡ {f:X→Y,…} ≤{f:AT→Z}
o que só é possível se X→Y≤AT→Z, ou seja Y≤Z e AT≤X. Esta última asserção é já a conclusão
pretendida.
Esboçamos apenas a demonstração do caso geral. O resultado sai da definição de polaridade (definição 2.3.4.3-1) e do facto da asserção α≤β implicar que α tem uma estrutura tão
ou mais rica que β (i.e. a estrutura arbórea de β pode ser mergulhada na estrutura arbórea de α,
podendo esta última ter ramificações suplementares).
Seja S o conjunto de ocorrências negativas de SAMET em AI. S dá origem, por substituição
de SAMET por AT em AI, a S′, um conjunto de ocorrências negativas de AT em AT≡AI[AT] .
Como T≤AT, logo T tem uma estrutura mais rica do que AT e portanto existe uma ocorrência
84
OM – Uma linguagem de programação multiparadigma
negativa duma subexpressão de T por cada ocorrência de S′ . Essas subexpressões de T têm de
ser supertipos de AT pois tratam-se de ocorrências negativas (se fossem subtipos então, por
definição, seriam ocorrências positivas, o que é falso; se não estivessem relacionadas pela relação de subtipo então nunca teríamos T≤AT.
Teorema 5.3-13 Sejam
AI =ˆ INTERFACE(ϒ A), BI+ =ˆ INTERFACE(ϒ B), AT =ˆ OBJTYPE(ϒA)
e
BT =ˆ OBJTYPE(ϒB) e assumamos que todas as ocorrências de SAMET em BI+ são positivas. Caso
BI+ tenha sido obtido a partir de AI substituindo todas as ocorrências negativas de SAMET por
AT (ou BT, pois são iguais), e não alterando mais nada, então:
1. Os tipos AT e BT são iguais:
AT = BT
2. BI+ é uma extensão válida da interface AI:
BI+ ext AI
3. Tomando uma classe a com a interface AI e uma sua subclasse imediata b com interface
BI+, então todo o método binário f definido em a com o tipo SAMET→τ, tem de ser redefinido na subclasse b com o tipo BT→τ . Existem infinitas formas seguras de redefinir f .
Vamos sugerir uma forma por defeito que tem três vantagens: (1) pode ser gerada automaticamente; (2) invoca a versão original de f ; (3) trata apenas os casos previstos na
versão original de f (i.e. não toma decisões prematuras relativamente aos outros casos).
Eis a sugestão:
f=λx:BT.if checkType[SAMET] x then super.f (downcast BT [SAMET] x) else diverge τ :BT→τ
No lado direito da função, divergeτ representa a computação divergente de tipo τ (cf.
secção 2.3.3.1).
Nota: Este teorema proporciona uma técnica que permite transformar uma classe a, cujas subclasses não geram subtipos, numa outra classe b, subclasse imediata de a , que produz objectos
do mesmo tipo da primeira e cuja subclasses geram sempre subtipos (pelo teorema 5.3-11).
Prova: Como temos de tratar de forma distinta as ocorrências negativas e positivas de SAMET
em AI, vamos alterar a formulação original de AI e usar agora dois parâmetros: um chamado
POS, representando as ocorrências positivas de SAMET, e outro chamado NEG, representando as
ocorrências negativas de SAMET. Seja AI * a nova formulação da interface AI:
AI *
=ˆ ΛPOS.ΛNEG.ϒ A*
Comecemos por ver como AT , AI, BT, BI podem ser expressos à custa de AI* :
AT =
AI =
BT =
BI+ =
OBJTYPE(ϒA) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ A) = ΛSAMET.AI* [SAMET][SAMET]
OBJTYPE(ϒB) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ B) = ΛSAMET.AI* [SAMET][BT]
5 Tipo SAMET, relações de compatibilidade e de extensão
85
Note que o segundo argumento de AI * na interface BI+ não é SAMET mas sim BT, conforme
o enunciado do teorema.
1ª Parte: Como se pode observar AT e BT são sintacticamente idênticos. Assim podemos escrever:
AT = BT
2ª Parte: Partamos da hipótese T≤BI+ [T] para provar T≤AI[T] e concluir BI+ ext AI, como se
pretende.
Para começar, como BI+ não tem ocorrências negativas de SAMET, BI+ é um operador de
tipo crescente. Assim, aplicando a regra [Sub κµ] de F+ à hipótese T≤BI+[T] obtemos a seguinte
asserção que utilizaremos adiante:
T≤BT
Usando outra vez a hipótese T≤BI+ [T], obtemos sucessivamente:
T≤BI+[T]
= AI * [T][BT]
≤ AI * [T][T]
= AI[T]
por definição de BI+
porque T≤BT e BT ocorre negativamente
por definição de AI
Desta forma T≤AI[T], e portanto BI+ ext AI, como queríamos provar.
3ª Parte: O método f:SAMET→τ da classe a tem de ser redefinido com tipo BT→τ na classe b
para que a interface desta classe cumpra as condições do teorema. A sugestão do enunciado é
uma hipótese de redefinição pois tem o tipo BT→τ , como é fácil de confirmar.
A redefinição sugerida assume que a linguagem suporta operações dinâmicas de teste e despromoção de tipo, o que em última análise deve acontecer em toda a linguagem que suporte a
noção de subtipo (cf. secção 4.3.2). Mas é possível redefinir o método f de outras formas. No
entanto, se o programador quiser invocar a versão de f da superclasse com o argumento x:BT ,
então terá mesmo de usar tipificação dinâmica (a invocação simples (super.f x) estaria mal tipificada). Nestes casos provavelmente o programador usará um padrão semelhante ao exemplificado, substituindo apenas o termo divergeτ por um outro termo à sua escolha: ver o exemplo do
método eq introduzido na classe point2PC da secção 4.3 e redefinido na secção 4.3.4.
A redefinição de função f , sugerida no enunciado do teorema, tem a seguinte forma equivalente:
f=λx:BT.(super.f (downcast BT [SAMET] x))
:BT→τ
Demos mais relevo à outra versão complicada pois ela tem a virtualidade de explicitar o caso
em que a função aborta.
1ª Parte revisitada: Retomando a primeira parte do teorema, note que se não forem substituídas todas as ocorrências negativas de SAMET por BT em BI+, então não se poderia garantir que
86
OM – Uma linguagem de programação multiparadigma
BI+
estendia AI . Considere o seguinte contra-exemplo, onde apenas a primeira de duas ocorrências negativas de SAMET é substituída por BT:
AI
BT
BI
CT
=ˆ
=ˆ
=ˆ
=ˆ
INTERFACE({f:SAMET→Bool, g:SAMET→Bool})
OBJTYPE({f:SAMET→Bool, g:SAMET→Bool})
INTERFACE({f:BT→Bool, g:SAMET→Bool})
OBJTYPE({x:Nat, f:BT→Bool, g:SAMET→Bool})
Verifiquemos que CT≤BI[CT]⇒CT≤AI[CT]
/
:
CT≤BI[CT]
⇔ {x:Nat,f:BT→Bool,g:CT→Bool}≤{f:BT→Bool,g:CT→Bool}
⇔ verdade
CT≤AI[CT]
⇔ {x:Nat,f:BT→Bool,g:CT→Bool}≤{f:CT→Bool,g:CT→Bool}
⇔ CT≤BT
⇔ {x:Nat,f:BT→Bool,g:CT→Bool}≤{f:BT→Bool,g:BT→Bool}
⇔ BT≤CT
⇔ falso (pois CT tem mais componentes que BT)
Teorema 5.3-14 Sejam
AI =ˆ INTERFACE(ϒ A), BI =ˆ INTERFACE(ϒ B), AT =ˆ OBJTYPE(ϒA)
e
BT =ˆ OBJTYPE(ϒB). Caso BI tenha sido obtido a partir de AI, substituindo apenas algumas ocorrências positivas de SAMET por BT, então BI nunca poderá ser uma extensão válida de AI , não
obstante ser verdade que AT=BT.
Prova: Como temos de tratar de forma diferente um grupo de ocorrências positivas de SAMET
em AI (não necessariamente todas), vamos alterar a formulação original da interface AI por
forma que esta fique parametrizada em função dessas ocorrências. Para isso, introduzimos um
novo parâmetro POS , representando esse grupo ocorrências positivas de SAMET. Seja AI * a
nova formulação da interface AI :
AI *
=ˆ ΛPOS.ΛSAMET.ϒA*
Verifiquemos em primeiro lugar como AT , AI, BT, BI podem ser expressos à custa de AI* :
AT =
AI =
BT =
BI =
OBJTYPE(ϒA) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ A) = ΛSAMET.AI* [SAMET][SAMET]
OBJTYPE(ϒB) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ B) = ΛSAMET.AI* [BT][SAMET]
Note que o primeiro argumento de AI * na interface BI não é SAMET mas sim BT, conforme o
enunciado do teorema.
Pretendemos mostrar que não é verdade que BI ext AI. Para isso vamos construir um tipo CT
tal que CT≤ BI[CT] mas CT≤AI[CT]
/
.
Considere a interface CI obtida a partir de BI acrescentando-lhe uma nova componente qualquer:
5 Tipo SAMET, relações de compatibilidade e de extensão
CI
87
=ˆ ΛSAMET.AI**[BT][SAMET]
Seja CT o tipo gerado pela interface CI:
CT
=ˆ µSAMET.AI**[BT][SAMET]
Pelo teorema 5.3-10 sabemos que CI ext BI. Além disso, como CT≤CI[CT] (pelo lema 5.3-3)
podemos concluir CT≤BI[CT]. Falta só mostrar que CT≤AI[CT].
/
Se fosse verdade que CT≤AI[CT], então teríamos de ter, pelo lema 5.3-5,
AI **[BT][CT]≤AI * [CT][CT]
o que nos obrigaria a ter BT≤CT, porque estão em causa ocorrências positivas de BT e CT. Mas
isso é impossível pois CT tem mais componentes que BT.
Teorema 5.3-15 Sejam
BT =ˆ OBJTYPE(ϒB),
AI+ =ˆ INTERFACE(ϒ A), BI+ =ˆ INTERFACE(ϒ B), AT =ˆ OBJTYPE(ϒA)
AI +
e
BI+
onde todas as ocorrências de SAMET em
e
são positivas. Caso BI+
tenha sido obtido a partir de AI + substituindo apenas algumas ocorrências positivas de AT por
SAMET, então BI+ é uma extensão válida da interface AI, ou seja:
BI+ ext AI+
Nota: Este teorema terá pouca importância prática se for seguida a prática, metodologicamente
recomendada, de introduzir os métodos usando tipificação aberta no tipo do resultados: não é
por este facto que é posta em causa a geração de subtipos pelas subclasses, e assim potencia-se
a máxima reutilização do código.
Prova: Como temos de tratar de forma diferente um grupo de ocorrências positivas de AT em
AI + (não necessariamente todas), vamos alterar a formulação original de AI + por forma a que
esta fique parametrizada em função dessas ocorrências. Introduzimos, para isso, um novo
parâmetro POS , representando esse grupo de ocorrências positivas de AT. Seja AI* a nova formulação da interface AI+:
AI *
=ˆ ΛPOS.ΛSAMET.ϒA*
Vejamos primeiro como AT, AI+, BT, BI+ podem ser expressos à custa de AI* :
AT =
AI + =
BT =
BI+ =
OBJTYPE(ϒA) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ A) = ΛSAMET.AI* [AT][SAMET]
OBJTYPE(ϒB) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ B) = ΛSAMET.AI* [SAMET][SAMET]
Note que o primeiro argumento de AI* na interface AI+ não é SAMET mas sim AT, conforme
o enunciado do teorema.
Partamos da hipótese T≤BI+[T] para provar T≤AI +[T] e concluir BI+ ext AI+, como se pretende.
88
OM – Uma linguagem de programação multiparadigma
Para começar, como BI+ não tem ocorrências negativas de SAMET, BI+ é um operador de
tipo crescente. Assim, aplicando a regra [Sub κµ] de F+ à hipótese T≤BI+[T] obtemos T≤BT. Mas
como além disso AT=BT, obtemos a seguinte asserção que será utilizada adiante:
T≤AT
Retomando novamente a hipótese T≤BI+[T], obtemos sucessivamente:
T≤BI+[T]
= AI * [T][T]
≤ AI * [AT][T]
= AI[T]
por definição de BI+
porque T≤AT e AT ocorre positivamente
por definição de AI
Desta forma T≤AI+[T] e, portanto, BI+ ext AI+, como queríamos provar.
Teorema 5.3-16 Sejam as interfaces AI =ˆ INTERFACE(ϒ A),
BI =ˆ INTERFACE(ϒ B),
e os tipos-objecto AT =ˆ OBJTYPE(ϒA) e BT=ˆ OBJTYPE(ϒB). Caso BI tenha sido obtido a partir de AI, substituindo algumas ocorrências negativas de AT por SAMET, então em caso algum BI pode ser uma
extensão válida de AI, não obstante ser verdade que AT=BT.
Prova: Esta demonstração é quase idêntica à demonstração do teorema 5.3-14.
Como temos de tratar de forma diferente um grupo de ocorrências negativas de AT em AI
(não necessariamente todas), vamos alterar a formulação original de AI por forma a que esta
fique parametrizada em função dessas ocorrências. Para isso, introduzimos um novo parâmetro
NEG , representando esse grupo de ocorrências negativas de AT. Seja AI * a nova formulação da
interface AI :
AI *
=ˆ ΛSAMET. ΛNEG.ϒA*
Vejamos em primeiro lugar como AT, AI , BT, BI podem ser expressos à custa de AI * :
AT =
AI =
BT =
BI =
OBJTYPE(ϒA) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ A) = ΛSAMET.AI* [SAMET][AT]
OBJTYPE(ϒB) = µSAMET.AI* [SAMET][SAMET]
INTERFACE(ϒ B) = ΛSAMET.AI* [SAMET][SAMET]
Note que o segundo argumento de AI * na interface AI não é SAMET mas sim AT, conforme o
enunciado do teorema.
Pretendemos mostrar que não é verdade que BI ext AI. Para isso vamos construir um tipo CT
tal que CT≤ BI[CT] mas CT≤AI[CT]
/
.
Considere a interface CI obtido a partir de BI acrescentando-lhe uma nova componente
qualquer:
CI
=ˆ ΛSAMET.AI**[SAMET][SAMET]
Seja CT o tipo gerado pela interface CI:
5 Tipo SAMET, relações de compatibilidade e de extensão
CT
89
=ˆ µSAMET.AI**[SAMET][SAMET]
Pelo teorema 5.3-10 sabemos que CI ext BI. Além disso, como CT≤CI[CT] (pelo lema 5.3-3)
podemos concluir CT≤BI[CT]. Falta só mostrar que CT≤AI[CT].
/
Se fosse verdade que CT≤AI[CT], então teríamos de ter, pelo lema 5.3-5,
AI **[CT][CT]≤AI * [CT][AT]
o que nos obrigaria a ter AT≤CT, porque estão em causa ocorrências negativas de AT e CT. Mas
isso é impossível pois CT tem mais componentes que AT.
5.4 O operador “+”
O teorema 5.3-13 mostra que, tomando uma classe qualquer, é possível gerar automaticamente
uma sua subclasse imediata que fixa todas as ocorrências negativas de SAMET: Transformam-se assim todos os métodos binários herdados em métodos não binários. Agora, todas as subclasses da nova classe já geram subtipos, de acordo com o teorema 5.3-11.
Nesta secção introduzimos um novo operador unário prefixo “+”, aplicável a classes, que
gera automaticamente a subclasse prevista no teorema 5.3-11. Nesta secção introduzimos também a noção de método binário transformado.
Definição 5.4-1 (Classe +c) Seja uma classe c com interface CI e geradora do tipo objecto
CT:
CI
CT
=ˆ ΛPOS.ΛNEG.ϒ c
=ˆ µSAMET.CI[SAMET][SAMET]
Definimos +c como sendo a nova classe com interface +CI e geradora do tipo objecto +CT que a
seguir se descreve:
+CI
+CT
+c
=ˆ ΛSAMET.CI[SAMET][CT]
=ˆ CT
=ˆ class\c {f–= –––––––––––––––––––––––––––––––––––––––––––
λx:CT.(super.f (downcast CT [SAMET] x))}
Nesta última linha, os nomes –f representam os nomes de todos os métodos binários de c .
Os factos mais importantes sobre a classe +c são os seguintes:
• A classe +c pode ser gerada automaticamente, bastando agora escrever “+c” para temos
acesso a ela;
• As classes c e +c geram rigorosamente o mesmo tipo-objecto (cf. teorema 5.3-13);
• Todas as subclasses de +c geram subtipos (cf. teorema 5.3-11).
Definição 5.4-2 (Métodos binários transformados) Quando se define a classe +c a
partir duma classe c , sabemos que todos os métodos binários de c são automaticamente substi-
90
OM – Uma linguagem de programação multiparadigma
tuídos em +c por novos métodos gerados a partir dos primeiros. A estes novos métodos damos
o nome de métodos binários transformados.
Note que quando um método binário transformado é invocado com um argumento que não
tem o tipo do receptor, então o programa aborta por efeito do termo divergeτ (cf. teorema
5.3-13).
Usando o operador “+ ”, a classe b do exemplo do final da secção 5.1.3 pode agora ser definida simplesmente como b =ˆ +a.
5.4.1 Problemas que o operador “+”resolve
O operador “+” resolve o problema da reutilização de classes contendo métodos binários,
em contextos que requerem que as suas subclasses gerem subtipos. Note bem: Este operador
pode mesmo ser aplicado a classes distribuídas sob a forma de código objecto, portanto a classes cujo código fonte não está disponível.
O operador “+ ” vem também libertar o programador da obrigação de ter de antecipar com
demasiado rigor a futura utilização das suas classes. Assim, será sempre possível, mesmo recomendado, começar por usar liberalmente tipificação aberta nas classes, por forma a maximizar
as possibilidades de reutilização do código dessas classes. Mais tarde, se for necessário criar
uma versão de alguma dessas classes sem métodos binários, basta aplicar a essa classe o operador “+” para obter a classe pretendida.
5.4.2 Ilustração duma aplicação de “+”
Nesta secção, vamos apresentar um exemplo simples que ilustra a utilidade do operador “+”.
Suponhamos que pretendemos definir um conjunto de classes que representem, e permitam
manipular, formas geométricas a duas dimensões. Vamos introduzir uma classe abstracta shape,
que captura a ideia abstracta de forma geométrica, e duas suas subclasses concretas, rectangle e
oval, que capturam a funcionalidade de duas formas geométricas específicas. No que se segue,
assumimos que as três classes, shape, rectangle e oval, geram respectivamente os tipos-objecto
ShapeT , RectangleT, e OvalT .
Na biblioteca de classes da linguagem OM final, existe uma classe de biblioteca chamada
equality que implementa o método de igualdade genérico eq:SAMET→Bool . Vamos definir as
nossas classes, shape, rectangle e oval como subclasses de equality para tirar partido desse método.
Considerando o que foi dito, o nosso sistema de classes terá a seguinte organização:
equality
shape
rectangle
oval
=ˆ
=ˆ
=ˆ
=ˆ
class\object { eq=λx:SAMET.(…), neq=λx:SAMET.(not (self.eq x)) }
class\equality { … }
class\shape { … }
class\shape { … }
5 Tipo SAMET, relações de compatibilidade e de extensão
91
Como se pode observar, as classes shape, rectangle, oval herdam efectivamente o método eq de
equality. No entanto levanta-se a questão esperada: a natureza do nosso problema faz com que
seja natural, mesmo desejável, que os tipos RectangleT e OvalT sejam subtipos de ShapeT : só
desta forma, uma expressão do tipo RectangleT, por exemplo, poderá ser usada num contexto
onde se espera uma expressão do tipo ShapeT . Contudo, o facto de shape herdar métodos binários de equality impede as subclasses de shape de gerarem subtipos.
Uma solução para este problema consistiria em não herdar de equality. Mas podemos fazer
melhor do que isso usando o operador “ +”. Assim, vamos substituir a definição original de
shape por esta outra:
shape
=ˆ class\+equality { …
eq=λx:ShapeT.if checkType[SAMET] x
then super.f (downcastBT [SAMET] x)
else false }
A nova classe shape tem duas virtualidades. Em primeiro lugar herda directamente de
+equality, o que resolve o anterior problema da geração de subtipos pelas suas subclasses. Em
segundo lugar redefine o método de igualdade duma forma que serve as necessidades das suas
subclasses e de tal forma que estas não têm necessidade de redefinir o método. Note como o
tipo SAMET ocorre na condição (checkType[SAMET] x), um tipo cujo significado é automaticamente ajustado nas subclasses.
Analisemos a funcionalidade da nova versão do método eq . Ele começa por testar se o seu
argumento é do mesmo tipo que o receptor. Se for do mesmo tipo (imagine que estamos a
comparar dois elementos do tipo RectangleT) então é invocado o método de igualdade originalmente definido em equality (através do método binário transformado eq de +equality). Se não for
do mesmo tipo (imagine que estamos a comparar um elemento do tipo RectangleT com um do
tipo OvalT), a função retorna imediatamente o valor false.
Se redefiníssemos o método eq como eq=λx:ShapeT.(super.f x) na classe em shape, a nova
versão estaria estaticamente bem tipificada, mas existiria o perigo do método binário transformado eq de +equality ser invocado com um argumento de tipo distinto do tipo do receptor: por
exemplo, numa mensagem (re.eq ov) com re:RectangleT e ov:OvalT. O teste de tipo introduzido na
nossa redefinição de eq, (checkType[SAMET] x) , resolve o problema.
5.4.3 Eficácia da classe +c na prática
É necessário verificar se a transformação dos métodos binários de c em métodos não binários
de +c prejudica em alguma medida a reutilização do código de c nas subclasses de +c. Para
simplificar a discussão, vamos começar por admitir que os métodos binários transformados de
+c não são redefinidos em nenhuma das futuras subclasses de +c.
92
OM – Uma linguagem de programação multiparadigma
5.4.3.1 Métodos binários transformados não redefinidos
Para começar, todos os métodos da superclasse c que nela dependiam dos métodos binários de
c, ao serem herdados pelas subclasses de +c, passam a depender dos métodos binários transformados de +c. Neste caso tudo funciona bem, pois as restrições estáticas associadas ao uso dos
métodos binários originais mantêm-se relativamente aos métodos binários transformados (i.e.
o teste de tipo dinâmico efectuado nos métodos transformados produz sempre true ). Além disso, como através de super, cada método binário transformado invoca o correspondente método
binário original, não será por culpa dos métodos binários transformados que a semântica do
código herdado de c muda.
Resta-nos verificar se, nas subclasses de +c, existe algum problema associado a novos usos
dos métodos herdados.
Primeiro, relativamente a todos os métodos não binários de c, eles continuam disponíveis
nas subclasses de +c (pelo menos enquanto não forem redefinidos); não há qualquer problema
especial a apontar à sua utilização.
Segundo, relativamente aos métodos binários de c, todos eles foram alvo de transformação
em +c. É preciso cautela com estes métodos. Se for necessário invocar algum deles a partir de
código novo numa subclasse de +c, então o programador terá de garantir dinamicamente que o
tipo do argumento x tem o tipo do receptor da mensagem (ou seja (checkType[SAMET] x)). Se
esta condição não se verificasse então seria activada a expressão diverge e o programa abortaria. É portanto natural que o programador queira redefinir os métodos binários transformados
nas subclasses directas de +c.
Relativamente a este “problema da invocação, a partir de código novo, de métodos binários
transformados não redefinidos”, convém que o compilador considere como erro todas as invocações de métodos binários transformados não redefinidos que não estejam protegidas por um
teste dinâmico de tipo da forma (checkType[τ] x), onde τ é o tipo do receptor da mensagem
(geralmente SAMET).
5.4.3.2 Métodos binários transformados redefinidos
Nas subclasses de +c, não há qualquer problema em redefinir os métodos binários transformados. Inclusivamente, essa redefinição é recomendada pois elimina o “problema da invocação
de métodos binários transformados não redefinidos a partir de código novo”.
5.4.3.3 Conclusão
As subclasses de +c reutilizam de forma efectiva todo o código da classe original c. É apenas
necessário tomar algumas precauções relativamente à invocação dos métodos binários transformados que surgem na classe +c.
5 Tipo SAMET, relações de compatibilidade e de extensão
93
Há três atitudes correctas perante os métodos binários transformados: (1) não se usam; (2)
redefinem-se; (3) usam-se, mas só sob uma garantia, dada por checkType, de que o argumento
tem o tipo do receptor.
A atitude preferível é a da redefinição.Mas repare que se o método redefinido quiser aceder
à funcionalidade do método original, como a versão do método eq na classe shape da secção
5.4.2, tem de usar a técnica (3).
5.5 Discussão sobre L5
A introdução de suporte para o nome SAMET em L5 veio corrigir algumas deficiências do mecanismo de herança de L4. O mecanismo de herança ficou mais flexível, passando a suportar a
definição de subclasses que em L4 não era possível definir. Na linguagem L5, o programador
tem a possibilidade de exprimir com mais precisão as suas intenções ao definir as suas classes.
No entanto dois novos problemas surgem em L5.
O primeiro problema consiste na complexidade da relação de extensão entre interfaces: é
fácil criar situações em que é difícil para o programador, ou para os leitores dos programas,
verificar se uma interface estende, ou não estende, uma outra interface. Não parece boa ideia
obrigar os utilizadores da linguagem a compreender todas as implicações duma relação de extensão intrincada. Propomos uma solução para este problema no ponto 5.5.1, já a seguir.
O segundo problema, já aflorado na secção 5.1.3, tem a ver com o facto das possibilidades
de programação genérica ficarem prejudicadas pela ausência de garantias quanto à geração de
subtipos pelas subclasses duma classe. A introdução do operador “+”, na secção anterior, ajuda a resolver este problema, mas convém aprofundar um pouco mais o tópico da “programação genérica em L5”. É isso o que faremos no ponto 5.5.2.
5.5.1 Complicação da relação de extensão
O seguinte exemplo, apesar de relativamente simples, já obriga a uma verificação não trivial
da condição BI ext AI.
XT
AI
BI
=ˆ OBJTYPE({x:Nat}) = {x:Nat}
=ˆ INTERFACE({x:Nat, f:Nat→XT})
=ˆ INTERFACE({x:Nat, y:Nat, f:Nat→SAMET})
A prova de que BI ext AI faz-se da seguinte forma: tomando uma variável de tipo T tal que
T≤{x:Nat, y:Nat, f:Nat→T}, como {x:Nat, y:Nat, f:Nat→T}≤{x:Nat}, logo por transitividade obtemos
T≤{x:Nat}. Agora, pela regra [Sub →] obtemos {x:Nat, y:Nat, f:Nat→T}≤{x:Nat, f:Nat→{x:Nat}}
donde, por transitividade, sai T≤{x:Nat, y:Nat, f:Nat→{x:Nat}} , como pretendíamos.
Este exemplo mostra que a relação de extensão ext, por ser muito geral, permite a criação
de programas obscuros em que a relação de extensão entre uma classe e as suas subclasses
94
OM – Uma linguagem de programação multiparadigma
pode ser difícil de entender. Naturalmente, programas que usem a relação desta forma serão
difíceis de manter.
Na prática, convém restringir as possibilidades de definição de subclasses directas em L5:
Restrição 5.5.1-1 Dada uma classe c , apenas as seguintes subclasses directas de c são consideradas bem formadas:
1 - As subclasses directas de c que mantêm as decisões de tipificação relativas a SAMET;
2 - A classe +c.
Esta restrição sobre subclasses imediatas pode ser capturada pela seguinte relação de extensão ext2:
[ext2 ≤]
Γ ΛX.κ≤ ΛX.κ′
Γ
ΛX.κ ext2 ΛX.κ′
[ext2 +]
ΛX+. ΛX-.κ :∗⇒∗⇒∗
Γ ΛX.κ[X][(µX.κ[X][X])] ext2 ΛX.κ[X][X]
Γ
A primeira regra corresponde à relação de subtipo entre interfaces ≤, referida no teorema
5.3-10. Esta regra obriga a respeitar as decisões de tipificação, aberta ou fechada, tomadas na
superclasse imediata.
A segunda regra permite substituir todas as ocorrências negativas de SAMET pelo tipo-objecto fixo gerado pela subclasse. A subclasse está de acordo com a primeira parte do enunciado do teorema 5.3-13 e caracteriza-se pelo facto de todas as suas subclasses serem geradoras
de subtipos (cf. teorema 5.3-11). Esta regra permite o uso do operador “ +”.
A relação ext2 está bem fundada sobre a relação ext, como indicam os teoremas 5.3-10 e
5.3-13. Adoptamos a relação ext2 ao nível do sistema de tipos da linguagem. De qualquer
forma, ao nível do modelo semântico, não há vantagem em substituir a relação ext pela relação
mais estrita ext2. Aliás, a relação ext2 seria insuficiente descrever para uma relação de herança
satisfatória pelo facto de não ser transitiva.
5.5.2 Programação genérica
A linguagem L4 suporta polimorfismo de inclusão, uma forma de polimorfismo que depende
da relação de subtipo. Aliás, em L4, todas as funções beneficiam desta forma de polimorfismo,
na medida em que uma função que aceite argumentos dum tipo τ também aceita argumentos
dum subtipo τ′≤τ.
O polimorfismo de inclusão é um mecanismo da maior importância em L4, devido ao facto
da relação de herança estar ligada à relação de subtipo em L4. É usando polimorfismo de
inclusão que, em L4, se podem escrever funções aplicáveis a todos os objectos gerados pelas
subclasses duma dada classe.
5 Tipo SAMET, relações de compatibilidade e de extensão
95
Também está disponível polimorfismo de inclusão em L5, mas o facto de nesta linguagem
as subclasses nem sempre gerarem subtipos reduz o âmbito de aplicação deste mecanismo.
Efectivamente, existem muitas classes para as quais não é possível escrever funções aplicáveis
a todos os objectos gerados pelas suas subclasses. Este é um problema essencial que importa
resolver e que trataremos na linguagem L6. Em L6, introduziremos uma forma de polimorfismo paramétrico restringido, baseado na relação de compatibilidade entre um tipo e uma interface de L5 que permite a definição de funções paramétricas aplicáveis a todos os objectos gerados pelas subclasses duma dada classe.
A forma de polimorfismo paramétrico, baseado na relação de compatibilidade, a introduzir
em L6, resolverá certamente muitas das nossas necessidades de programação genérica. Mas
será que resolve todas as necessidades?
A resposta é negativa. Na secção 5.2.3 comparámos a relação de subtipo com a relação de
compatibilidade e verificámos que a segunda relação fica a perder relativamente à primeira no
aspecto da substitutividade. Ora acontece que existem formas recorrentes de programação genérica que dependem da possibilidade de usar valores dum tipo onde se esperam valores de
outro tipo. Como uma das nossas preocupações, relativamente à linguagem L5, consiste em
estudar e minorar as tensões existentes entre a relação de subtipo e as relações de compatibilidade e de extensão, este é o momento certo para analisar até que ponto a linguagem L5 consegue lidar com essas situações.
Os casos práticos de programação genérica que dependem da propriedade da substitutividade envolvem tipos heterogéneos ou colecções heterogéneas. Analisamos agora cada um destes
dois casos, para ver como eles se tratam em L5.
5.5.2.1 Tipo heterogéneo
Consideremos o conhecido problema da representação e manipulação de expressões algébricas usando a linguagem L5. A estrutura duma expressão algébrica pode ser adequadamente
representada usando uma árvore de nós heterogéneos, onde esses nós representam diferentes
categorias de entidades: operações algébricas de diferentes aridades, constantes inteiras, constantes reais, variáveis, etc. Para exemplificar, a expressão simples “1+x” pode ser representada
usando uma árvore constituída por três nós: a raiz desta árvore é um nó binário aditivo, que
tem como subárvore esquerda um nó constante, representando o número 1, e como subárvore
direita um nó variável, representando a variável x.
Sendo necessário definir vários tipos de nós, todos variantes duma ideia geral de nó de
árvore de expressão, a sua descrição pode ser convenientemente organizada numa hierarquia
de classes com três níveis: na raiz encontra-se uma classe abstracta node que captura a noção
geral de nó de árvore de expressão; no segundo nível encontram-se três classes abstractas,
binNode, unNode , zeroNode, que, respectivamente, capturam as ideias, um pouco menos gerais, de
nó binário (nó contendo duas subárvores), nó unário (nó contendo uma única subárvore) e nó
96
OM – Uma linguagem de programação multiparadigma
sem filhos; finalmente, no terceiro nível encontram-se várias classes ditas concretas, cada uma
delas definindo completamente um tipo específico de nó: addNode e subNode (subclasses de
binNode), simNode (subclasse de unNode ), iconstNode, rconstNode e varNode (subclasses de zeroNode).
Vamos designar por NodeT, BinNodeT, AddNoteT, etc, os tipos-objectos gerados respectivamente
pelas classes node, binNode , addNode, etc.
Chamamos tipo heterogéneo a um tipo cujos valores podem assumir formas diversas. Cada
elemento com uma forma particular é um elemento do tipo heterogéneo, o que significa que,
do ponto de vista lógico, os vários tipos que descrevem as várias formas do tipo heterogéneo
são subtipos deste. Por exemplo, o tipo-objecto NodeT, gerado pela classe node, é um tipo heterogéneo, por decisão de construção, sendo os tipos BinNodeT , AddNoteT, etc. gerados pelas subclasses de node subtipos de NodeT.
É fácil ver que sem este requisito de subtipo, ou requisito de substitutividade, não seria possível construir certos elementos de NodeT. Consideremos o caso da construção dum nó aditivo,
do tipo AddNoteT: a construção é efectuada a partir de dois nós do tipo NodeT, devendo ser possível a utilização de nós de tipos concretos, e.g. AddNodeT, VarNodeT, ConstNodeT, onde se esperam nós do tipo heterogéneo NodeT.
Mas o requisito da substitutividade não permite que a classe node contenha qualquer método
binário. Felizmente esse aspecto não é problema pois, por imperativo lógico, espera-se que
todo o método que receba outra árvore como argumento tenha o seu argumento declarado com
o tipo fixo NodeT, e não com o tipo aberto SAMET. Pensando, por exemplo, numa operação de
comparação de expressões (i.e. no método de igualdade), a versão desta operação definida ao
nível da classe addNode deve estar preparada para estabelecer comparação com outras árvores
quaisquer, portanto com raiz do tipo mais geral NodeT, e não apenas com árvores cuja raiz seja
apenas do tipo AddNodeT.
A necessidade de dispor duma noção de subtipo surge naturalmente no problema da representação e manipulação de expressões algébricas que acabámos de apresentar e discutir.
Assim, este exemplo serve para argumentarmos que a nossa linguagem deve continuar a
suportar a noção de subtipo. É certo que a possibilidade de definir métodos binários vem introduzir complicações, mas o operador “+ ” foi criado para nos permitir lidar com estas complicações.
No contexto do problema que temos vindo a discutir vamos considerar uma complicação
induzida pelos métodos binários que o operador “+ ” resolve. Suponhamos que a nossa classe
node precisa de herdar duma classe x:
node
=ˆ class\x {…}
mas que, por mero acaso, entre os métodos de x se encontram alguns métodos binários.
5 Tipo SAMET, relações de compatibilidade e de extensão
97
Imediatamente, cai pela base toda a solução apresentada anteriormente, pois as subclasses
de node deixam de gerar subtipos de NodeT. É aqui que o operador “ +” vem em nosso socorro:
se node herdar de +x e não de x, tudo se resolve:
node
=ˆ class\+x {…}
5.5.2.2 Colecções heterogéneas
Uma colecção heterogénea é constituída por objectos de tipos diferentes, definidos independentemente uns dos outros, mas possuindo algumas características comuns. Eis dois exemplos
de colecções heterogéneas: (1) uma lista de objectos heterogéneos que, apesar disso, suportam
um método de escrita chamado print; (2) um array onde são guardadas as janelas duma interface gráfica: as janelas têm funcionalidade variável mas suportam um núcleo de operações
comuns.
Nas colecções heterogéneas também se requer o uso de substitutividade, pois objectos de
diferentes tipos têm de ser guardados numa estrutura de dados, a qual tem de se comprometer
com algum tipo para os objectos a guardar.
O tratamento duma colecção heterogénea é simples se não ocorrerem métodos binários no
núcleo de operações comuns dos objectos a guardar na colecção. Nesta circunstância o tipo
mais geral que captura essas operações comuns é supertipo de todos os tipos considerados na
colecção.
Relativamente ao caso em que ocorrem métodos binários no núcleo de operações comuns,
nem a linguagem L5, nem mesmo a linguagem OM, final, oferecem uma solução específica
que não passe pela utilização de tipificação dinâmica. Se for mesmo necessário manter os métodos binários, podemos proceder da seguinte forma: numa primeira fase deixamos de considerar os métodos binários como pertencentes ao núcleo de operações comuns e aplicamos a receita do caso anterior. Depois, perante a necessidade de aplicar um método binário a um objecto a usando um objecto b como argumento, usamos a operação dinâmica de teste de tipo para
validar dinamicamente a operação e a operação dinâmica de despromoção de tipo para preparar a sua realização.
O problema da definição duma colecção heterogénea em que o núcleo comum de operações
contém métodos binários não é solúvel usando um sistema de tipos estático convencional.
Quando se aplica um método binário a um objecto a duma colecção usando outro objecto b da
mesma colecção como argumento, o conhecimento dos tipos de a e b é sempre parcial. Assim
não é possível decidir estaticamente se o tipo de a é igual ao tipo de b, uma condição necessária para que o método binário seja aplicável ao par. Uma solução envolveria uma análise global do fluxo da execução, mas isso ultrapassa os limites dum sistema de tipos convencional.
98
OM – Uma linguagem de programação multiparadigma
5.6 Conclusões
É notável como a simples introdução do nome SAMET nas classes de L5 conseguiu marcar tão
significativamente a linguagem: as possibilidades de reutilização de código aumentaram; a
capacidades de expressão das intenções do programador melhorou; foi necessário recorrer a
novas noções para explicar a linguagem. Um aspecto negativo é o facto da linguagem L5 ser
ficado um pouco mais complicada do que a linguagem L4, mas esse é o preço normal a pagar
pelas capacidades de expressão acrescidas.
Considerando a literatura sobre sistemas de tipos estáticos para linguagens orientadas pelos
objectos, o trabalho ao qual a linguagem L5 mais deve é o trabalho de Cook, Hill e Canning
[CHC90]. Neste trabalho estuda-se um modelo geral de herança e mostra como um mecanismo
de herança flexível pode comprometer a geração de subtipos pelas subclasses. Outros trabalhos próximos do nosso, por também envolverem o estudo de mecanismos de herança, são
[Bru94, ESTZ94, PT94, BPF97, BSG95, BFSG98,AC96]. Estes trabalhos diferem muito nas
técnicas de formalização adoptadas: semântica denotacional, semântica operacional, codificação em cálculo-lambda polimórfico.
Tipicamente, nos trabalhos referidos, a relação de subclasse baseia-se directamente na relação de subtipo entre operadores de tipo (cf. regra [Sub Λ ]). Isso significa que nesses trabalhos
se coloca todo o ênfase no mecanismo de herança, sendo deixada para segundo plano a relação
de subtipo. Os aspectos práticos da conciliação dos dois mecanismos são assim ignorados.
O único trabalho que se preocupa com este aspecto é [BPF97]. Relativamente à linguagem
LOOM, introduzida nesse trabalho, os autores adoptam uma solução interessante: prescindem
da relação de subtipo, e introduzem um mecanismo de substitutividade alternativo, baseado na
relação de subclasse – o mecanismo dos hash types. Um hash type é um tipo polimórfico com
a forma #τ e com a seguinte característica essencial: dado um tipo-objecto τ gerado por uma
classe c, num contexto onde se espera uma expressão do tipo #τ, pode escrever-se uma expressão de tipo tipo-objecto σ que seja gerado por uma subclasse de c . Assim, na linguagem
LOOM, “herança implica substitutividade” (NB: não implica subtipificação). Contudo os hash
types têm uma limitação importante: relativamente a expressões com tipo polimórfico #τ, não
se permite a invocação de métodos binários; só é possível a invocação de métodos binários de
expressões com tipo monomórfico.
O trabalho [ESTZ94] apresenta a linguagem LOOP. Este trabalho merece-nos uma menção
especial pelo facto de usar uma relação de herança mais geral do que os outros, apesar dela
nunca chegar a ser explicitada, ficando implícita e difusa nas regras do sistema de tipos. É
também o único trabalho que discute a possibilidade de se usar tipificação aberta ou tipificação
fixa nas classes, alegando-se, inclusivamente, que as decisões de tipificação fixa/aberta tomadas ao nível das superclasses poderiam ser arbitrariamente alteradas nas subclasses. No entanto o sistema de tipos apresentado, e cuja correcção se prova, não está acordo com esta alega-
5 Tipo SAMET, relações de compatibilidade e de extensão
99
ção. Aliás, nunca poderia estar, pois só pondo em causa o princípio da “reutilização sem reverificação” é que se poderia alcançar a total liberdade de revisão do código.
A principal contribuição deste capítulo, referente à linguagem L5, tem a ver com o problema da minimização das tensões entre o mecanismo de herança e a relação de subtipo. Para resolver o problema, introduzimos uma relação de extensão entre interfaces de classes ext, o
mais geral possível, estudámos as suas propriedades, e descobrimos e provámos a possibilidade de introduzir o operador “+ ” descrito na secção 5.4. Finalmente, introduzimos algumas restrições à utilização da relação ext, apenas por uma questão pragmática de simplificação da linguagem: na verificação de qualquer subclasse imediata usamos a regra [Sub Λ], tal como na
maioria dos trabalhos que acabámos de referir, com a diferença que introduzimos uma regra
adicional que fornece suporte específico para o operador “+”.
Capítulo 6
Polimorfismo paramétrico
Sintaxe dos géneros,tipos e termos de L6
Κ ::= ∗ | ∗⇒Κ
τυϕϒI::= Bool | Nat | υ→τ | X |
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′ |
SAMET | CLASSTYPE(ϒ) | INTERFACE(ϒ) | OBJTYPE(ϒ) |
∀ X≤*INTERFACE(ϒ B).τ
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
self | super | class R | class\s R | new c | o.l |
checkType[τ] | downcastσ[τ] |
λX≤*INTERFACE(ϒ B).e | P[τ]
Semântica dos tipos
∀ X≤*INTERFACE(ϒ B).τ
=ˆ
tipo universal ≤*-restringido
∀ X≤INTERFACE(ϒ B)[X].τ
ˆ ϕ[τ]
ϕ[τ] =
instanciação de tipo universal ≤*-restringido
Semântica dos termos
ˆ
λX≤*INTERFACE(ϒ B).e =
abstracção paramétrica ≤*-restringida
λX≤INTERFACE(ϒB)[X].e
=ˆ P[τ]
instanciação de abstracção paramétrica ≤*-restringida
P[τ]
Instanciação duma entidade paramétrica S com uma variável de tipo X
ˆ λX≤*Is.e
S=
ˆ λX≤*Ic.S[X]
C=
*Restrição implícita: Ic ext Is
(cf. secção 6.2.2)
Classes paramétricas (caso particular)
∀ X≤*INTERFACE(ϒ B).CLASSTYPE(ϒ c) tipo das classes paramétricas
ΛX.OBJTYPE(ϒ c)
∀ X≤*INTERFACE(ϒ B).class
tipo-objecto paramétrico (gerado por classe paramétrica)
Rc
classe paramétrica
Na secção 6.1, introduzimos o conceito polimorfismo paramétrico ≤ * -restringido e ilustramos as suas aplicações mais importantes. Na secção 6.2, discutimos a formalização do polimorfismo paramétrico ≤* -restringido e quais requisitos de boa tipificação das respectivas equações semânticas. Na secção 6.3, discutimos e solucionamos certos problemas práticos ligados
à utilização de polimorfismo paramétrico em L6. Na secção final, 6.4, tiramos algumas conclusões e relacionamos a nossa variedade de polimorfismo paramétrico com trabalhos de
outros autores.
102
OM – Uma linguagem de programação multiparadigma
6.1 Conceitos e mecanismos de L6
Na linguagem funcional L6, introduzimos uma forma de polimorfismo paramétrico restringido
em que a restrição sobre a variável de tipo se baseia na relação de compatibilidade ≤* entre um
tipo-objecto e uma interface (cf. definição 5.2-11). Chamaremos a esta forma de polimorfismo,
“polimorfismo paramétrico ≤* -restringido”. Uma entidade paramétrica ≤* -restringida tem a
forma P =ˆ λX≤* INTERFACE(ϒ B).e, com tipo ∀ X≤* INTERFACE(ϒ B).τ. Pode ser instanciada com
qualquer tipo-objecto σ que inclua todas as operações indicadas na interface-limite, ou baliza,
INTERFACE(ϒ B). A operação de instanciação da entidade paramétrica P com o tipo-objecto σ é
denotada por P[τ].
No caso duma entidade paramétrica ≤ * -restringida P ter como interface-limite a interface
duma classe c, então P pode ser instanciada com qualquer tipo-objecto gerado por uma subclasse de c . Assim, P consegue operar de forma genérica sobre todos os objectos gerados pelas
subclasses de c .
O polimorfismo paramétrico ≤ * -restringido de L6 tem três aplicações importantes: (1) permite definir classes paramétricas; (2) permite definir funções paramétricas; (3) permite resolver o problema da definição de métodos em que o tipo dos parâmetros é alvo de especialização
(tratam-se dos chamados parâmetros covariantes). Vamos discutir estas três aplicações nas
três subsecções seguintes.
6.1.1 Classes paramétricas
Uma classe paramétrica considera-se um construtor de classes por servir para a construir classes simples (não-paramétricas) através da operação instanciação. Na linguagem L9, introduziremos uma segunda forma de construtor de classes, o modo (cf. 9.1.3).
Em função do tipo usado para instanciar o seu tipo-parâmetro, uma classe paramétrica origina classes não-paramétricas distintas, as quais, por sua vez, geram tipos-objecto simples distintos. Ora os tipos-objecto gerados dependem dos argumentos de instanciação da classe paramétrica, pelo que é razoável dizer que uma classe paramétrica gera um tipo-objecto paramétrico. Esse tipo-objecto paramétrico é uma função de tipos-objecto para tipos-objecto, ou seja,
é um operador de tipo com uma forma particular.
A título de ilustração, vamos analisar a seguinte classe paramétrica bag e a sua subclasse paramétrica set:
EqI
bag
set
=ˆ INTERFACE({eq:SAMET→Bool})
=ˆ λX≤* EqI.class{Rbag}
=ˆ λX≤* EqI.class\bag[X]{Rset}
:∗⇒∗
:∀X≤* EqI.CLASSTYPE(ϒ bag)
:∀X≤* EqI.CLASSTYPE(ϒ bag⊕ϒset)
Nesta equações, EqI:∗⇒∗ representa a interface-limite de ambas as classes; Rbag:ϒbag é o registo
das componentes da classe bag ; Rset:ϒset é o registo das componentes novas ou modificadas da
subclasse set.
6 Polimorfismo paramétrico
103
Note que as interfaces de bag e set, respectivamente BagI e SetI , são tipos paramétricos pois
dependem da variável de tipo X:
BagI
SetI
=ˆ ΛX.INTERFACE{ϒbag}
=ˆ ΛX.INTERFACE{ϒbag⊕ϒset}
:∗⇒∗⇒∗
:∗⇒∗⇒∗
Os tipos-objecto gerados por bag e set, respectivamente BagT e SetT, são também tipos paramétricos dependentes da mesma variável X:
BagT
SetT
=ˆ ΛX.OBJTYPE{ϒbag}
=ˆ ΛX.OBJTYPE{ϒbag⊕ϒset}
:∗⇒∗
:∗⇒∗
Como indica a interface-limite EqI , as classes paramétricas bag e set podem ser aplicadas a
tipos-objecto que definam publicamente uma igualdade eq . Por exemplo:
bagPoint3
setPoint2
=ˆ bag[Point3T]
=ˆ set[Point2T]
:OBJTYPE(ϒbag[Point3T/X])
:OBJTYPE((ϒbag⊕ϒset)[Point3T/X])
Estas duas classes implementam colecções homogéneas que armazenam, respectivamente, objectos de tipo Point3T e objectos de tipo Point2T.
Diz-se que set é uma extensão paramétrica bem formada da classe bag sse para todas as possíveis instanciações de set, se verificar que a interface de set[X] estende a interface de bag[X] .
Esta condição escreve-se:
X≤ * EqI ⇒ SetI[X] ext BagI[X]
ou equivalentemente:
X≤EqI[X] ⇒ (T≤SetI[X][T] ⇒ T≤BagI[X][T])
6.1.2 Funções paramétricas
Apresentamos seguidamente uma função paramétrica search que sabe procurar objectos do tipo
X em objectos do tipo BagT[X], onde X representa um tipo qualquer que defina uma igualdade
eq. BagT é o tipo-objecto paramétrico gerado pela classe paramétrica bag da secção anterior.
EqI
BagT
search
=ˆ INTERFACE({eq:SAMET→Bool})
=ˆ ΛX.OBJTYPE(ϒbag)
=ˆ λX≤* EqI.λx:X.λb:BagT[X].(e search)
:∀X≤* EqI.X→BagT[X]→τsearch
A função search tem três parâmetros. O primeiro parâmetro representa o tipo X≤ * EqI dos elementos a pesquisar. O segundo parâmetro representa o elemento x:X a pesquisar. O terceiro
parâmetro é o objecto b:BagT[X] no qual a procura é efectuada. Eis um exemplo de invocação
de search :
search[Point2T] (new (point2PC 4 5)) (new bag[Point2T])
A definição de search pode ser adaptada por forma a constituir um método da classe bag. Vejamos como é que a definição da função search seria adaptada ao contexto da classe bag :
104
OM – Uma linguagem de programação multiparadigma
bag
=ˆ λX≤* EqI.class{…, search=λx:X.{esearch}, …}
Note que no novo contexto, a função search não precisa de declarar os parâmetros X e b. Isto resulta do facto de, dentro da classe bag, todos os métodos já se encontram implicitamente parametrizados por um tipo X e por um valor chamado self , do tipo SAMET.
6.1.3 Parâmetros covariantes
Nas linguagens L5 e L6, o tipo SAMET tem um tratamento privilegiado, no sentido em que é o
único tipo que, ocorrendo negativamente no tipo dum método (neste caso binário), pode variar
da superclasse para uma subclasse de forma covariante.
Existem exemplos que mostram que seria útil poder alargar esta flexibilidade de SAMET aos
outros tipos da linguagem. Infelizmente não existe uma solução directa para este problema que
respeite a relação de extensão ext entre interfaces. Contudo, existe uma técnica indirecta que
permite obter grande parte do efeito desejado através do uso de classes paramétricas. Trata-se
duma técnica conhecida na literatura [Coo89, Sha94, Sha95], que todo o programador de OM
teria vantagem em conhecer. Vamos ilustrá-la, adaptando um exemplo de Shang [Sha94].
Considere a duas classes food e animal com interfaces FoodI e AnimalI e geradoras dos tipos-objecto FoodT e AnimalT:
FoodT
FoodI
food
AnimalT
AnimalI
animal
=ˆ OBJTYPE(ϒfood)
=ˆ INTERFACE(ϒ food)
=ˆ class{Rfood}
=ˆ OBJTYPE({eat:FoodT→Nat, …})
=ˆ INTERFACE({eat:FoodT→Nat, …})
=ˆ class{eat=λx:FoodT.eeat, …}
Vamos assumir que a classe animal contém um método eat que aceita um objecto do tipo FoodT
(um alimento a ingerir) e retorna um número natural (por exemplo, o número de mooooos de
satisfação emitidos pelo animal, se se tratar duma vaca).
Suponha agora que necessitamos de introduzir uma nova categoria de animais, um pouco
mais particular do que a definida pela classe animal: por exemplo, uma categoria de carnívoros.
Para isso criamos uma classe carnivoreAnimal na qual assumimos que os carnívoros comem alimentos duma nova categoria FleshFoodT, sendo FleshFoodT≠FoodT.
FleshFoodT
FleshFoodI
fleshFood
CarnivoreAnimalT
CarnivoreAnimalI
carnivoreAnimal
=ˆ OBJTYPE(ϒfood⊕ϒfleshfood)
=ˆ INTERFACE(ϒ food⊕ϒfleshFood)
=ˆ class\food{RfleshFood}
=ˆ OBJTYPE({eat:FleshFoodT→Nat, …})
=ˆ INTERFACE({eat:FleshFoodT→Nat, …})
=ˆ class{eat=λx:FleshFoodT.eeat2, …}
6 Polimorfismo paramétrico
105
Tecnicamente, não há qualquer problema com estas definições. Mas repare que evitámos
definir a classe carnivoreAnimal como subclasse de animal. Por uma boa razão: tal definição não
seria válida. Se a tentássemos tal definição, o tipo do argumento do método eat evoluiria da
classe animal para a classe carnivoreAnimal de forma covariante, o que iria contra a relação de
extensão ext: a relação ext exigiria que FleshFoodT fosse um supertipo de FoodT, o que é false.
Usando classes paramétricas, vejamos então como se consegue fazer com que a classe
carnivoreAnimal seja subclasse de animal e, ao mesmo tempo, tornar o parâmetro do método eat
covariante. Para isso temos de parametrizar as duas classes de animais em função dos tipos dos
alimentos que os animais de cada classe podem ingerir:
animal
carnivoreAnimal
=ˆ λF≤ * FoodI.class{eat=λx:F.e eat, …}
=ˆ λF≤ * FleshFoodI.class\animal[F] {eat=λx:T.eeat2, …}
Note que para que estas definições estejam bem tipificadas é necessário que a interface que
restringe o tipo dos alimentos dos carnívoros – FleshFoodI – seja mais estrita do que a interface
que restringe o tipo dos alimentos dos animais – FoodI – ou seja, FleshFoodI ext FoodI. Só assim
carnivoreAnimal[F], com F≤ * FleshFoodI , pode ser subclasse de animal[F].
O nosso objectivo está cumprido. No entanto será instrutivo expandir um pouco mais o
exemplo anterior, considerando agora o problema da definição de uma classe conjunto de animais, animalSet, e duma sua subclasse conjunto de carnívoros: carnivoureAnimalSet . Para isso precisamos de usar classes duplamente paramétricas: a classe animalSet deve poder ser instanciada
com qualquer tipo de alimento F e com qualquer tipo de animal A que coma esse alimento; a
classe carnivoureAnimalSet deve poder ser instanciada com qualquer tipo de alimento F adequado
a carnívoros e com qualquer tipo de carnívoro A que coma esse alimento. Eis uma solução:
AnimalI
CarnivoreAnimal
animalSet
carnivoreAnimalSet
=ˆ
=ˆ
=ˆ
=ˆ
∀ F≤ * FoodI.INTERFACE({eat:F→Nat, …})
∀ F≤ * FleshFoodI.INTERFACE({eat:F→Nat, …})
λF≤ * FoodI.λA≤ * AnimalI[F].class{…}
λF≤ * FleshFoodI.λA≤* CarnivoreAnimalI[F].class\animalSet[F][A]{…}
O problema da covariância a que dedicámos esta subsecção sugere-nos um comentário final
sobre a leitura lógica do tipo dos parâmetros dos métodos das classes animal . Na classe animal
original, não-paramétrica, podemos dizer que o tipo do argumento do método eat=λx:FoodT.eeat
está quantificado universalmente: realmente, todo o objecto da classe animal é obrigado a aceitar qualquer alimento do tipo FoodT, independentemente da categoria específica a que o animal
pertença. Já na versão final, paramétrica, da classe animal, o tipo do argumento do método
eat=λx:F.eeat pode considerar-se quantificado existencialmente: para cada categoria de animais,
é já possível especificar uma categoria de alimentos F adequada a esses animais.
106
OM – Uma linguagem de programação multiparadigma
6.2 Semântica de L6
Nesta secção, vamos considerar a formalização dos tipos e dos termos de L6 através das construções de F +.
6.2.1 Semântica dos tipos e termos
É particularmente simples a introdução em L6 da sua variedade de polimorfismo paramétrico.
A relação ≤ * , usada para restringir os tipos-argumento das abstracções paramétricas de L6, tem
tradução imediata para F+ (cf. definição 5.2-11), o que significa que o polimorfismo paramétrico ≤* -restringido de L6 se pode reduzir de forma imediata ao polimorfismo paramétrico F-restringido de F+.
Como ilustração, o significado do termo P de L6:
P
=ˆ λT≤* EqI.λx:T.λs:set[T].e
:∀T≤ * EqI.T→et[T]→τ
é dado pelo seguinte termo, P′, de F+:
P′ = λT≤EqI[T].λx:T.λs:set[T].e
:∀T≤EqI[T].T→et[T]→τ
6.2.2 Boa tipificação da instanciação com variáveis de
tipo
Curiosamente, no contexto da linguagem L6, a relação de extensão entre interfaces ext (cf. definição 5.2.2.2-1) reaparece no contexto da verificação da boa tipificação da instanciação de
abstracções paramétricas P com variáveis de tipo X: P[X].
Consideremos as duas abstracções paramétricas S e C:
S
C
=ˆ λY≤* Is .e
=ˆ λX≤* Ic.S[X]
Sob que condições é que a instanciação S[X] , efectuada dentro da abstracção C, está bem tipificada? A condição necessária e suficiente é a seguinte, X≤ * Ic⇒X≤ * Is , ou seja as duas interfaces-limite, Ic e I s , envolvidas na instanciação devem estar relacionadas na propriedade:
Ic ext Is
Em L6, para além de variáveis de tipo introduzidas nas abstracções paramétricas existe ainda a variável de tipo predefinida SAMET. Este é o caso que analisamos seguidamente.
Consideremos novamente a abstracção paramétricas S:
S
=ˆ λY≤* Is .e
Sob que condições é que a instanciação S[SAMET] , efectuada dentro duma classe c, está bem
tipificada? As equações semânticas de L5 mostram que uma classe de L5 (e L6) se formaliza
6 Polimorfismo paramétrico
107
usando uma entidade paramétrica ≤* -restringida que está dependente dum tipo-parâmetro chamado SAMET. Ora, na classe c , o tipo-parâmetro SAMET é introduzido em sujeito à, já familiar,
restrição SAMET≤ * INTERFACE(ϒ c), onde INTERFACE(ϒc) representa a interface da classe c . Assim, podemos concluir que SAMET poderá ser usado na instanciação de S sse for possível deduzir SAMET≤* Is de SAMET≤* INTERFACE(ϒ c), ou seja sse a interface-limite Is verificar a condição:
INTERFACE(ϒ c) ext Is
6.3 Discussão sobre L6
O polimorfismo paramétrico de L6 constitui um poderoso mecanismo cujas aplicações mais
importantes já foram ilustradas nas secções 6.1.1, 6.1.2, e 6.1.3. Uma outra possível aplicação
seria a parametrização duma classe em função dum tipo-objecto genérico, representativo de todas as possíveis extensões dessa mesma classe. Mas esta ideia já foi implicitamente incorporada nas classes de L5: recordamos que a variável de tipo SAMET foi introduzida para representar esse mesmo tipo-objecto genérico.
Se na linguagem L5, a relação de compatibilidade ≤ * era apenas usada nas equações semânticas da linguagem, já na linguagem L6 ela passa a poder ocorrer explicitamente nos programas, mais exactamente na definição de entidades polimórficas. Relativamente a este uso explícito de ≤* , lembramos que X≤ * I é simples açúcar sintáctico para a asserção X≤I[X]. A ideia é
que quando se escreve X≤* I, fica subentendido que o significado de SAMET dentro de I é X .
Justifica-se a introdução desta notação pelo facto da expressão X≤ * I ser menos complexa do
que a expressão X≤I[X]. Por exemplo, a primeira forma será certamente menos confusa e intrigante do que a segunda para o programador iniciado.
A utilização prática do polimorfismo paramétrico de L6, cria algumas necessidades novas a
que dedicaremos as duas subsecções seguintes. Felizmente essas necessidades conseguem ser
completamente satisfeitas no âmbito de L6, sem que seja necessário modificar a linguagem.
6.3.1 Operações dependentes do tipo-parâmetro
No contexto duma entidade paramétrica P =ˆ λX≤* INTERFACE(ϒ B).e, por vezes sente-se a falta
duma constante do tipo X que possa ser usada na inicialização de variáveis locais do tipo X ,
ou, então, sente-se a falta duma função de criação de objectos do tipo X que permita organizar
uma estrutura de dados complexa constituída por objectos do tipo X. Em geral, seria conveniente que no contexto de P , estivesse à disposição do programador um registo de constantes e
funções ops: OpsT[X] de tipo dependente de X, para serem usados consoante as necessidades.
Felizmente consegue-se resolver este problema usando apenas as construções de L6. Basta
adicionar à entidade paramétrica um segundo parâmetro ops, de tipo OpsT[X], ficando a entidade paramétrica com a seguinte nova configuração:
108
OM – Uma linguagem de programação multiparadigma
P
=ˆ λX≤* I.λops:OpsT[X].e
:∀X≤* I.OpsT[X]→σ
Se τ for um tipo-objecto tal que τ≤ * I, e se r for um objecto do tipo OpsT[τ], então a aplicação
de P a τ e a r processa-se assim:
P[τ]r = e[τ/X,r/ops]
:σ[τ/X]
Este problema tem interessantes ligações com os meta-objectos da linguagem L8 (cf. secção 8.3.1).
6.3.2 Polimorfismo paramétrico e coerções
O mecanismo dos modos, a introduzir em L9 para ser usado na linguagem OM final, requer
que a linguagem OM disponha dum sistema de conversões implícitas de tipo (coerções) que,
em particular, permita converter o modo das expressões em função do contexto onde elas
ocorrem (cf. secções 9.1.2.2 e 10.1.3). A praticabilidade do mecanismo dos modos depende da
existência dum tal sistema de coerções, sendo o capítulo 10 dedicado ao desenvolvimento
deste sistema.
Para além destas conversões de tipo ligadas ao uso de expressões, precisamos ainda de permitir que certas coerções possam ser aplicadas durante a instanciação de entidades paramétricas, com o fim de legitimar algumas instanciações, à partida proibidas. Veremos um pouco
mais adiante, no exemplo 2 da secção 6.3.2.2, que a efectividade do mecanismo dos modos
requer a presença dum mecanismo de instanciação generalizada de entidades paramétricas,
devendo esse mecanismo usar coerções.
As questões ligadas à introdução e uso deste mecanismo de instanciação generalizada são
tópico da presente secção e respectivas subsecções. Na primeira subsecção, vamos discutir
abstractamente o problema e propor uma solução. Na segunda subsecção, ilustramos o nosso
método por meio de dois exemplos.
6.3.2.1 Problema e solução
Considere a entidade paramétrica genérica P:
P
=ˆ λX≤* I.e
:∀X≤* I.τ
Imagine que pretendemos instanciar P com um tipo υ que, não sendo imediatamente compatível com a interface-limite INTERFACE(ϒ B), possa ser tornado compatível com ela por acção
duma coerção prevista na linguagem.
Vamos formalizar as condições desta hipotética instanciação, assumindo que no contexto Γ
da instanciação de P estão definidas as seguintes entidades:
- um tipo auxiliar σ, tal que Γ σ≤ * I;
- a coerção Γ υ≤cσ;
- a função de conversão associada da coerção anterior: (Γ υ≤cσ) :υ→σ.
6 Polimorfismo paramétrico
109
A técnica que usamos para permitir a instanciação de P com υ, atribuindo assim um significado à expressão, à partida errada, P[υ] , envolve a incorporação de parametrização extra em P:
adicionamos-lhe um novo parâmetro de tipo A para representar o tipo auxiliar σ, e um novo
parâmetro c:C→A para representar a função de conversão (Γ υ≤cσ). Representemos por P′ esta
reescrita de P . Eis como fica P′ :
P′
=ˆ λX≤* {}.λA≤* I.λc:X→A.e′
:∀X≤* {}.∀ A≤* I.X→A.τ′
No corpo da nova abstracção, é assumido que fica disponível a coerção ∅,X≤* {},A≤ * I X≤cA
(com função de conversão associada c) para ser aplicada a qualquer expressão do tipo X, sempre que necessário. Em P′ representámos por e′ a reescrita de e que resulta da acção desta coerção.
A instanciação de P com υ, originalmente impossível, define-se agora da seguinte forma:
P[υ] =ˆ P′[υ][σ] (Γ υ≤cσ)
Chamaremos a esta forma de instanciação, instanciação generalizada com coerção.
Note que esta forma de alargar o âmbito de aplicação das entidades paramétricas de L6 por
meio da introdução de parametrização extra, pode ser automaticamente aplicada pelo compilador da linguagem a todas as entidades paramétricas que ocorrem nos programas. Não existe o
perigo de pecar por excesso: a instanciação normal não é mais do que um caso particular da
instanciação generalizada, e pode sempre ser obtida usando a coerção identidade (Γ υ≤cυ),
como se mostra:
P[υ] =ˆ P′[υ][υ] (Γ υ≤cυ)
6.3.2.2 Exemplos
Descritos o problema e respectiva solução, vamos ilustrar o nosso método por meio de dois
exemplos: o primeiro, muito simples e um pouco artificial; o segundo mais realista e um
pouco mais complicado.
Exemplo1: Considere a interface-limite I, a função paramétrica P que depende dessa interface-limite, e o tipo υ:
I
P
υ
=ˆ INTERFACE({a:Int→Bool})
=ˆ λX≤* I.{b=λx:X.(x.a 1)}
=ˆ {a:Float→Bool}
:∀X≤* I.{b:X→Bool}
É fácil ver que neste caso a instanciação directa P[υ] não é possível. Vamos por isso aplicar
o método estudado na secção anterior, começando por introduzir o seguinte tipo auxiliar:
σ
=ˆ {a:Int→Bool}
110
OM – Uma linguagem de programação multiparadigma
Este tipo é compatível com a interface-limite I e verifica a condição Γ υ≤cσ (o que é fácil de
verificar usando as regras da secção 10.3.2). Assim reescrevemos P como P′ e P[υ] como P σ[υ]
da seguinte forma:
P′
P σ[υ]
=ˆ λX≤* {}.λA≤* I.λc:X→A.{b=λx:X.((c x).a 1)}
=ˆ P′[υ][σ] (Γ υ≤cσ)
E pronto! O problema ficou resolvido. Note como no corpo de P′ foi necessário aplicar a
coerção c:X→A ao argumento x:X.
Exemplo2: Neste segundo exemplo, considere a interface-limite EqI , um tipo υ compatível com
esta interface, e o tipo LazyT υ gerado pela classe lazy υ (cf. secção 9.2.2.2):
EqI =ˆ INTERFACE({eq:SAMET→Bool})
υ =ˆ {eq:SAMET→Bool, …}
LazyT υ = {eq:υ→Bool, …}
Suponhamos, que pretendíamos aplicar, o modo log ao tipo LazyT υ, num contexto Γ, com o
fim de obter a classe log LazyT υ Este é um desejo razoável pois o tipo LazyT υ tem uma
igualdade definida (originária de υ), e o requisito básico que o modo log impõe aos seus tipos-argumento é que definam uma igualdade. Além disso a igualdade do tipo LazyT υ pode ser
usada na maioria dos contextos sem que surja qualquer problema. É apenas no contexto da instanciação duma entidade paramétrica particular – o modo log – que surge um obstáculo: a interface-limite deste modo é EqI , e o tipo LazyT υ não é tecnicamente compatível com ela. Por
esta razão, o (pré-)tipo log LazyT υ tem de ser considerado mal formado, à partida.
Problemas desta natureza têm de ser resolvidos, sob pena do mecanismo dos modos ficar
prejudicado por restrições de utilização que comprometem muito a sua utilidade prática. A solução que propomos passa, mais uma vez, pelo recurso ao método da subsecção 6.3.2.1, embora, no caso presente tenhamos de o aplicar ao nível da equação semântica dos modos. Sendo a
equação original que define um modo na linguagem L9 a seguinte:
mode X≤* INTERFACE(ϒ B).RM
=ˆ λX≤* INTERFACE(ϒ B).
class{access(pub(ϒB)[X/SAMET]) + R M}
vamos reescrevê-la da forma:
mode′ X≤ * INTERFACE(ϒ B).RM
=ˆ λX≤* {}.λA≤* INTERFACE(ϒ B).λc:X→A.
class′{access(pub(ϒ B)[X/SAMET]) + R M}
No corpo da nova definição, a função de conversão c:X→A é implicitamente aplicada, sempre
que necessário, às expressões do tipo X que aí ocorram.
Para concluirmos a resolução do nosso problema inicial, temos de tirar ainda partido do
facto da asserção de coerção LazyT υ≤ cυ ser válida para qualquer tipo υ. Definimos então o tipo
log LazyT υ da seguinte forma elegante:
log lazy υ
=ˆ log′[LazyT υ][υ] (Γ LazyT υ≤cυ)
6 Polimorfismo paramétrico
111
Resolvido o problema, justificam-se alguns comentários finais sobre a essência do problema e da solução encontrada.
Para começar, note que o modo lazy não define qualquer igualdade da sua responsabilidade:
este modo limita-se a proporcionar um acesso indirecto à igualdade definida no seu objecto
conexo. Esse acesso indirecto revela-se suficiente na maioria das situações em que a igualdade
é usada. Contudo não foi suficiente para que o tipo log LazyT υ pudesse ser definido.
Solucionámos o problema através da criação dum acesso directo à igualdade definida no
objecto conexo. Efectivamente a função de conversão c:(LazyT υ)→υ, quando usada dentro do
modo log, não faz mais do que proporcionar este acesso: por exemplo, por sua acção, a expressão a=b, com a:LazyT υ e b:LazyT υ, é convertida na expressão (c a)=(c b) , a qual já compara directamente dois elementos de υ, sendo υ≤ * EqI.
6.4 Conclusões
Na literatura, a referência mais antiga a polimorfismo paramétrico restringido encontra-se no
trabalho de Cardelli e Wegner [CW85], tendo surgido no contexto duma tentativa de modelização de mecanismos de linguagens orientadas pelos objectos usando a relação de subtipo. A
restrição desta forma de polimorfismo baseava-se exactamente na relação de subtipo. As limitações deste sistema (cf. secção 2.1.4) levaram à criação do polimorfismo paramétrico
F-restringido [CCH+89], o qual foi inicialmente usado implicitamente na modelização de herança, mas rapidamente surgiu como mecanismo explicito em algumas linguagens: Emerald
[BHJL86], Sather [Omo92], k-bench [San93].
Nas linguagens, POLYToil [BSG95, BFSG98] e LOOM [BPF97], Bruce introduziu uma
relação de matching entre tipos-objecto e usou-a na definição semântica do mecanismo de
herança. Definiu depois uma forma de polimorfismo paramétrico baseada nessa relação. A relação de matching trata os tipos-objecto como operadores de tipo parametrizados numa variável de tipo MyType (semelhante à nossa variável SAMET). Sendo essa relação de matching virtualmente idêntica à relação de subtipo entre operadores de tipo (cf. regra [Sub Λ ]), o polimorfismo baseado naquela relação é, na sua essência, polimorfismo restringido de ordem
superior (em que o limite superior é definido por um operador de tipo).
A nossa relação de extensão ext é diferente (mais geral) da relação de matching de Bruce.
Naturalmente, a nossa versão de polimorfismo paramétrico baseia-se na relação ext, ou mais
exactamente na relação prévia de compatibilidade ≤* , pois um dos nossos objectivos foi usar o
polimorfismo paramétrico como mecanismo de programação genérica para operar sobre todos
os objectos gerados pelas subclasses duma dada classe.
Neste capítulo julgamos ter ilustrado bem as aplicações mais importantes do polimorfismo
paramétrico ≤* -restringido da linguagem L6. De qualquer forma muitas destas aplicações não
diferem de forma essencial de aplicações semelhantes que ocorrem noutras formas de polimor-
112
OM – Uma linguagem de programação multiparadigma
fismo paramétrico. Isso é verdade nas situações em que o que importa é a capacidade genérica
de definir abstracções sobre variáveis de tipo, e não a forma particular das restrições de instanciação a que essas abstracções estão submetidas.
No final deste capítulo resolvemos algumas questões complexas ligadas à compatibilização
mútua dos seguintes três mecanismos da linguagem OM: modos, coerções, polimorfismo paramétrico ≤* -restringido.
Capítulo 7
Componentes privadas e variáveis de
instância
Sintaxe dos géneros,tipos e termos de L7
Κ ::= ∗ | ∗⇒Κ
τυϕϒI::= Bool | Nat | υ→τ | X |
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′ |
SAMET | CLASSTYPE(ϒ) | INTERFACE(ϒ) | OBJTYPE(ϒ) |
SELFT | GINTERFACE(ϒ) | IINTERFACE(ϒ) | SINTERFACE(ϒ) | IOBJTYPE(ϒ) |
∀ X≤*INTERFACE(ϒ B).τ |
priv(ϒ) | pub(ϒ) | all(ϒ)
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
self | super | class R | class\s R | new c | o.l | priv_new c |
checkType[τ] | downcastσ[τ] |
λX≤*INTERFACE(ϒ B).e | P[τ] |
priv(R) | pub(R) | all(R)
Tipos-registo parciais
ˆ tipo-registo das componentes privadas
priv(ϒ c) =
pub(ϒ c)
all(ϒ c)
=ˆ
=ˆ
tipo-registo das componentes públicas
ϒ c = tipo-registo das componentes privadas e públicas
Semântica dos tipos
GINTERFACE(ϒ c) :∗⇒∗
ΛSAMET.ΛSELFT.ϒ c
IINTERFACE(ϒ c) :∗⇒∗
=ˆ
=ˆ
ΛSAMET.ΛSELFT.all(ϒ c)
SINTERFACE(ϒ c) :∗⇒∗
=ˆ
interface global
interface interna
(= GINTERFACE(ϒ c))
interface secreta
ΛSAMET.ΛSELFT.priv(ϒ c)
INTERFACE(ϒ c) :∗⇒∗
=ˆ
interface externa
ΛSAMET.pub(ϒ c)
IOBJTYPE(ϒ c) :∗
=ˆ
tipo-objecto interno
µSELFT.IINTERFACE(ϒ c)[OBJTYPE(ϒ c)][SELFT]
(= all(ϒ c)[OBJTYPE(ϒ c)/SAMET,IOBJTYPE(ϒ c)/SELFT])
ˆ
OBJTYPE(ϒ c) :∗ =
tipo-objecto externo
µSAMET.INTERFACE(ϒ c)[SAMET] (= pub(ϒ c)[OBJTYPE(ϒ c)/SAMET])
*Verifica-se: IOBJTYPE(ϒ c)≤OBJTYPE(ϒ c)
(teorema 7.2.1-1)
114
OM – Uma linguagem de programação multiparadigma
CLASSTYPE(ϒ c) :∗
=ˆ
tipo-classe
∀ SAMET≤*INTERFACE(ϒ c).
∀ SELFT≤*IINTERFACE(ϒ c)[SAMET].
(SELFT→SAMET)→
SELFT→
IINTERFACE(ϒ c)[SAMET][SELFT]
Semântica das relações
Relação binária de extensão geral de L7 entre interfaces globais
ˆ
GINTERFACE(ϒ c) gen_ext GINTERFACE(ϒ s) =
*
*
(T≤ INTERFACE(ϒ c) ⇒ T≤ INTERFACE(ϒ s)) & SINTERFACE(ϒ c)≤SINTERFACE(ϒ s)
Registos parciais
ˆ registo das componentes privadas
priv(Rc) =
pub(Rc)
all(Rc)
=ˆ
=ˆ
registo das componentes privadas
Rc = registo das componentes privadas e públicas
Semântica dos termos
ˆ
class Rc :CLASSTYPE(ϒ c) =
*
λSAMET≤ INTERFACE(ϒ c).
λSELFT≤*IINTERFACE(ϒ c)[SAMET].
λhide:SELFT→SAMET.
λself:SELFT.
all(Rc)
ˆ
class\s Rc :CLASSTYPE(ϒ s⊕ϒc) =
let S:CLASSTYPE(ϒ s) = s in
λSAMET≤*INTERFACE(ϒ s⊕ϒc).
λSELFT≤*IINTERFACE(ϒ s⊕ϒc)[SAMET].
λhide:SELFT→SAMET.
λself:SELFT.
let super:IINTERFACE(ϒ s)[SAMET][SELFT] =
(S[SAMET][SELFT] hide self) in
super+all(Rc)
*Restrição implícita: ϒ s,ϒ c devem ser tais que:
GINTERFACE(ϒ s⊕ϒc) gen_ext GINTERFACE(ϒ s)
ˆ
priv_new c :IOBJTYPE(ϒ c) =
let C:CLASSTYPE(ϒ c) = c in
let hide:IOBJTYPE(ϒ c)→OBJTYPE(ϒc) = λx:IOBJTYPE(ϒ c).((λy:OBJTYPE(ϒ c).y) x) in
let gen:IOBJTYPE(ϒ c)→IOBJTYPE(ϒc) = (C[OBJTYPE(ϒ c)][IOBJTYPE(ϒ c)] hide) in
let priv_o:IOBJTYPE(ϒ c) = fix gen in
priv_o
ˆ
new c :OBJTYPE(ϒ c) =
let C:CLASSTYPE(ϒ c) = c in
let priv_o:IOBJTYPE(ϒ c) = priv_new C in
let hide:IOBJTYPE(ϒ c)→OBJTYPE(ϒc) = λx:IOBJTYPE(ϒ c).((λy:OBJTYPE(ϒ c).y) x) in
let o:OBJTYPE(ϒ c) = hide priv_o in
o
ˆ
o.l :τ =
let R:OBJTYPE(ϒ c) = o in
R.l
ˆ
o.l :τ =
let R:IOBJTYPE(ϒ c) = o in
R.l
7 Componentes privadas e variáveis de instância
115
Na primeira secção do presente capítulo, apresentamos e discutimos as principais ideias da
linguagem L7 a nível intuitivo. Depois, na secção 7.2, desenvolvemos as equações semânticas
de L7 e determinamos os requisitos para a sua boa tipificação. Na secção 7.3, introduzimos
uma versão de L7 com suporte para variáveis de instância mutáveis: esta versão imperativa de
L7 designa-se por L7& e são muitos os problemas técnicos que discutimos no seu âmbito. Finalmente, a secção 7.4 dá um pouco de perspectiva sobre a linguagem L7 e o seu modelo, relativamente a outras linguagens e modelos.
7.1 Conceitos e mecanismos de L7
Na linguagem L7, introduzimos uma partição das componentes das classes e respectivos
objectos, passando a distinguir entre componentes privadas e componentes públicas. Por definição, as componentes privadas dum objecto são acessíveis apenas a partir do interior do próprio objecto (usando o nome self), enquanto que as componentes públicas são acessíveis a partir do interior e a partir do exterior do objecto. Os objectos passarão a ter dois tipos atribuídos
simultaneamente: um tipo interno constituído pelas assinaturas de todas as suas componentes,
privadas e públicas; e um tipo externo constituído pelas assinaturas das suas componentes
públicas.
Introduzimos também o novo nome de tipo SELFT no contexto de cada classe. O nome
SELFT será usado para representar o tipo interno de self. O nome SAMET continuará a ser
usado, tal como em L5 e L6, para representar o tipo externo de self. O significado de SELFT e
SAMET coincidirá nas classes que possuam exclusivamente componentes públicas.
Neste capítulo introduzimos ainda variáveis de instância mutáveis. Isso será feito numa
variante imperativa de L7, que designaremos por L7&. Tratamos desta questão separadamente,
porque a formalização das variáveis de instância requer o uso de artifícios técnicos que complicam a semântica da linguagem, obscurecendo-a. A tabela das equações de L7& foi colocada
no final do capítulo, pelo facto das equações que a compõem não serem particularmente ilumi+
nantes. Formalizamos a linguagem L7 & no sistema F&
, introduzido na secção 2.5.6.
Praticamente todas as linguagens de programação práticas orientadas pelos objectos integram alguma forma de encapsulamento. Isso não surpreende se pensarmos que alguns dos
mais importantes requisitos da Engenharia de Software – segurança, modificabilidade local,
abstracção – são alcançados por meio de encapsulamento. A linguagem CLOS [DG87] é um
raro exemplo que não suporta encapsulamento: esta linguagem inclui multimétodos, os quais
não são compatíveis com barreiras de acesso à funcionalidade dos objectos.
A maioria das linguagens práticas orientadas pelos objectos também suportam variáveis de
instância mutáveis: Smalltalk, C++, Modula-3, Java, Eiffel, Sather, Objective Caml [LRVD99,
RV98]. A maioria das linguagens teóricas associadas aos modelos teóricos referidos na secção
116
OM – Uma linguagem de programação multiparadigma
5.6 não tem estado mutável. Nessas linguagens, a dita actualização dos objectos realiza-se
produzindo cópias modificadas deles.
7.1.1 Formas de encapsulamento
Relativamente à ocultação da parte interna dos objectos, existem variações ao nível do local
onde a barreira de encapsulamento é colocada (cf. [FM96]). Nem todas as linguagens adoptam
a mesma solução. As linguagem Smalltalk e C++ são paradigmáticas relativamente às variantes possíveis.
No caso da linguagem Smalltalk, uma barreira de encapsulamento envolve cada objecto,
logo desde o momento da sua criação. Assim, garante-se que as suas componentes privadas
nunca serão acedidas a partir doutros objectos.
Esta forma de encapsulamento é flexível no sentido em que permite que objectos do mesmo
tipo possam ser internamente diferentes. Podemos, por exemplo, introduzir um tipo-objecto
Matriz , único, mas implementar os seus valores usando duas classes distintas: uma especializada na representação de matrizes densas e outra especializada na representação de matrizes
esparsas. No que diz respeito a informação de interface, não há qualquer problema que as duas
classes tenham interfaces internas distintas.
No caso da linguagem C++, a barreira de encapsulamento é colocada à volta de cada classe
e não dos objectos individuais, comportando-se cada classe como um tipo abstracto de dados.
Desta forma, no contexto da sua classe-mãe, os objectos têm conhecimento da estrutura privada dos objectos irmãos. Inclusivamente, um objecto que tenha abandonado os limites da classe-mãe e regresse ao seu contexto (como argumento dum método, por exemplo) continua a ser
reconhecido como um objecto dessa mesma classe.
Nesta forma de encapsulamento perde-se a vantagem da independência da parte privada
dos tipos-objecto. De facto, todas as classe que implementam um dado tipo-objecto são obrigadas a ter a mesma interface interna. Mas não há só desvantagens, já que o conhecimento da
interface interna traz algumas vantagens não desprezíveis: os métodos binários ficam com
acesso à parte privada dos seus argumentos; permite-se a inicialização de objectos a partir de
objectos irmãos (no contexto da classe-mãe); é possível aumentar a qualidade do código gerado pelo compilador (por existir mais informação de tipo disponível).
O C++ obriga ainda cada tipo-objecto a ser implementado numa única classe, mas esta é
outra questão.
A linguagem L7 adopta uma visão da encapsulamento próxima da visão do Smalltalk, mas
pontualmente influenciada pela visão do C++. Assim, em L7, tal como em Smalltalk, existe
uma barreira de encapsulamento intransponível protegendo a parte privada de cada objecto.
Abrimos, no entanto, uma excepção para objectos criados no contexto da sua própria classe-mãe. Um objecto criado no contexto da sua classe-mãe fica com a instalação da sua barreira
7 Componentes privadas e variáveis de instância
117
de encapsulamento adiada. Só a recebe mais tarde, se e quando for transferido para um contexto sintáctico exterior à classe-mãe: por exemplo, se for retornado por um método público.
Depois de recebida a barreira de encapsulamento a parte privada do objecto fica definitivamente oculta: mesmo que o objecto regresse mais tarde ao seio da classe-mãe, a sua parte privada já não poderá ser acedida pelos irmãos.
Esta variante de encapsulamento facilita a inicialização de objectos, particularmente por
parte dos construtores da linguagem L8. Além disso não compromete o seguinte princípio fundamental do Smalltalk que perfilhamos: “objectos do mesmo tipo devem poder ter partes privadas distintas”.
Entrando agora em detalhes, fora do contexto da sua classe-mãe, um objecto é sempre
criado com barreira, ou seja com um tipo externo, usando o operador new. Dentro do contexto
da sua classe-mãe, um objecto é sempre criado sem barreira, ou seja com o tipo interno SELFT ,
usando o operador priv_new. Relativamente à colocação da barreira nos objectos com tipo
SELFT, sempre que um deles é usado num contexto que requer uma expressão do tipo SAMET,
aplica-se ao objecto uma coerção hide:SELFT→SAMET, que faz com que o objecto passe a ter
associado um tipo externo.
O tipo interno SELFT está proibido de ocorrer na assinatura das componentes públicas das
classes (cf. secção 7.1.3). Basta esta regra para garantir que objectos com tipo SELFT não possam escapar do contexto da classe-mãe.
7.1.2 Nomeação das componentes das classes
Na nomeação das componentes das classes adoptaremos as seguintes convenções: os nomes
das componentes privadas distinguem-se por serem prefixados com “priv_”; os nomes das
componentes públicas distinguem-se por não serem prefixados com “priv_”.
Considerando um tipo-registo ϒ, usaremos a notação priv(ϒ) para representar o tipo-registo
parcial das componentes privadas de ϒ. A notação pub(ϒ) será usada para representar o tipo-registo parcial das componentes públicas de ϒ. Usaremos ainda a notação all(ϒ) para representar
o tipo-registo de todas as componentes de ϒ, ou seja o próprio ϒ . Portanto:
ϒ = priv(ϒ) ⊕ pub(ϒ)
ϒ = all(ϒ)
Relativamente aos registos, adoptamos convenções análogas. Assim, dado um registo R,
introduzimos os registos priv(R) , pub(R) e all(R) de forma idêntica. As seguintes equivalências de
termos são válidas para qualquer registo R:
R = all(ϒ) = priv(R) + pub(R)
Como ilustração, eis uma classe com duas componentes privadas, priv_x e priv_y:
118
OM – Uma linguagem de programação multiparadigma
pointC
=ˆ class { priv_x=0, priv_y=0,
sum=self.priv_x+self.priv_y,
priv_eq=λa:SELFT.(self.priv_x=a.priv_x & self.priv_y=a.priv_y) }
7.1.3 Tipo externo e tipo interno
Em L7, por razões técnicas, precisamos de associar dois tipos a cada objecto: um tipo-objecto
externo, que representa a visão externa do objecto e expõe apenas as suas componentes públicas, e um tipo-objecto interno, que representa a visão interna do objecto e expõe as suas componentes públicas e privadas.
Se ϒc for o tipo-registo das componentes dum objecto, o tipo interno desse objecto é representado por IOBJTYPE(ϒc) e o seu tipo externo é representado por OBJTYPE(ϒc). Para todo o
objecto, os seus tipos interno e externo verificam a condição: OBJTYPE(ϒc)≤IOBJTYPE(ϒc) (cf.
teorema 7.2.1-1):
No contexto dum tipo-objecto externo recursivo, OBJTYPE(ϒc), o nome SAMET tem um
significado fixo: ele representa uma referência recursiva ao próprio tipo-objecto externo. O
nome SELFT está proibido de ocorrer nos tipos-objecto externos, pelas razões que apresentamos no final desta secção.
No contexto dum tipo-objecto interno recursivo, IOBJTYPE(ϒc), o nome SELFT tem um
significado fixo: ele representa uma referência recursiva ao próprio tipo-objecto interno. O
nome SAMET também tem a liberdade de ocorrer num tipo-objecto interno onde, convencionalmente, representa o correspondente tipo-objecto externo.
No contexto duma classe, os nomes SAMET e SELFT têm um significado aberto: eles representam o tipo externo e o tipo interno de self. Como sabemos a interpretação das ocorrências
de self no código herdado varia através das subclasses.
Os tipos internos da forma IOBJTYPE(ϒ c) são tipos técnicos, introduzidos por razões técnicas
de definição da linguagem. Não se permite que sejam explicitamente usados nos programas. Já
os tipos externos da forma OBJTYPE(ϒc) não são tipos técnicos, e podem ser livremente usados
nos programas.
O tipo SELFT é considerado um tipo interno, e SAMET um tipo externo. Eles não são tipos
técnicos, e podem ser usados nos programas.
Como ilustração, o tipo interno e o tipo externo dos objectos da classe pointC, da secção
anterior, são respectivamente:
pointTi
pointT
=ˆ IOBJTYPE({priv_x:Nat, priv_y:Nat, sum:Nat, priv_eq:SELFT→Bool})
=ˆ OBJTYPE({priv_x:Nat, priv_y:Nat, sum:Nat, priv_eq:SELFT→Bool})
= OBJTYPE({sum:Nat})
É essencialmente por uma questão de consistência que se proíbe a ocorrência do nome
SELFT em qualquer tipo externo OBJTYPE(ϒ c). Se SELFT ocorresse no tipo duma componente
7 Componentes privadas e variáveis de instância
119
pública dum objecto particular x desse tipo, digamos numa componente p:Nat→SELFT, então a
mensagem (x.p 3) só poderia ser tipificada num contexto em que o tipo interno de x fosse
conhecido: ou seja, apenas dentro da classe de x. Mas nesse caso, p ficaria com as limitações
de acesso duma componente privada, o que seria inconsistente com o facto da componente p
ter sido introduzida como componente pública e de estar identificada como tal.
7.1.4 Interfaces global, externa, interna e secreta
Em L7, associamos quatro interfaces a cada classe: uma interface global que contém as assinaturas de todas as componentes da classe, uma interface externa que contém apenas as assinaturas das componentes públicas da classe, uma interface interna que contém as assinaturas das
componentes públicas e privadas da classe, e uma interface secreta que contém apenas as assinaturas das componentes privadas da classe.
Em L7 as interfaces global e interna coincidem, mas em L8 já não será assim.
Se ϒc for o tipo-registo das componentes duma classe, então a respectiva interface global
representa-se por GINTERFACE(ϒ c), a respectiva interface externa por INTERFACE(ϒc), a
respectiva interface interna por IINTERFACE(ϒc), e a respectiva interface secreta representa-se
por SINTERFACE(ϒc). Note que, como uma interface global GINTERFACE(ϒc) tem todas as componentes de ϒ c, ela consegue determinar univocamente as restantes três interfaces: a externa, a
interna e a secreta.
Numa interface externa o nome SAMET pode ocorrer, não sendo à partida alvo de qualquer
interpretação particular (cf. 4.1.4). O nome SELFT está proibido de ocorrer numa interface externa.
Numa interface global, interna ou secreta, os nome SAMET e SELFT podem ocorrer, não sendo à partida alvo de qualquer interpretação particular.
Uma classe com interface global GINTERFACE(ϒc) é uma classe geradora de objectos com
tipo externo OBJTYPE(ϒc) e com tipo interno IOBJTYPE(ϒ c).
É importante notar que, se o nome SELFT não pode ocorrer na interface externa duma classe, ele já tem liberdade de ocorrer nas outras interfaces, e também no corpo dos métodos
privados e públicos. Aliás, nem que seja implicitamente, o tipo SELFT é usado para tipificar self
no contexto de todos os método, privados e públicos. Quanto ao nome SAMET, ele tem a liberdade de ocorrer nas quatro interfaces e no corpo de todos os métodos.
7.1.5 SELFT, SAMET e herança
No contexto duma classe, os nomes SELFT e SAMET representam, respectivamente, o tipo
interno e o tipo externo de self . Os tipos SELFT e SAMET dizem-se tipos de significado aberto,
120
OM – Uma linguagem de programação multiparadigma
pois são ambos reinterpretados nas componentes herdadas, aliás, como já acontecia com
SAMET na linguagem L5.
Todas as questões que na linguagem L5 envolviam o nome SAMET e estavam ligadas às
tensões entre o mecanismo de herança e a relação de subtipo nessa linguagem não mudam em
L7: concretamente, continuam apenas a envolver componentes públicas e o nome SAMET. O
motivo é o seguinte: na linguagem L7, tal como pretendemos que ela seja vista pelo programador, a relação de subtipo entre tipos-objecto envolve apenas tipos externos.
Metodologicamente, em L7 deveremos tirar o máximo partido de tipificação aberta, ou seja
dos tipos SAMET e SELFT , na parte privada das classes. Assim se maximiza o potencial de reutilização do código privado herdado sem que, com isso, se comprometa a geração de subtipos
pelas subclasses!
Na linguagem L7, continuará a ser garantida a validade do princípio da “reutilização sem
reverificação”, mas agora aplicado tanto ao código herdado privado como ao código herdado
público. O cumprimento desse princípio é garantido através duma relação de extensão geral,
gen_ext (cf. definição 7.2.2.2-1) que define a boa forma das classes que podem ser definidas
em L7 usando herança. A nova relação generaliza a relação de extensão ext de L5.
7.2 Semântica de L7
Nesta secção, formalizamos e discutimos a semântica da linguagem L7. A tabela das equações
semânticas de L7 é apresentada no início do presente capítulo.
7.2.1 Semântica dos tipos
Neste ponto, discutimos a codificação das interfaces externas e internas das classe, dos tipos-objecto externos e internos, e dos tipos-classe.
Representa a interface global duma classe com componentes ϒc. Nesta
interface consideramos todas as componentes da classe. Os nomes SAMET e SELFT podem
ocorrer numa interface global. A formalização é ΛSAMET.Λ SELFT.ϒ c. Em L7 a interface global
coincide com a interface interna.
GINTERFACE(ϒc):
Representa a interface externa duma classe com componentes ϒc. Nesta
interface consideramos apenas as componentes públicas da classe. O nome SAMET pode ocorrer numa interface externa. Formaliza-se por meio do operador de tipo Λ SAMET.pub(ϒ c).
INTERFACE(ϒc):
Representa a interface interna duma classe com componentes ϒc. Nesta
interface consideramos as componentes privadas e públicas da classe, i.e. todas as componentes. Os nomes SAMET e SELFT podem ocorrer numa interface interna. A formalização deste tipo de L7 é ΛSAMET. ΛSELFT.all(ϒc). Em L7 a interface interna é equivalente à interface global.
IINTERFACE(ϒc):
7 Componentes privadas e variáveis de instância
121
Representa a interface secreta duma classe com componentes ϒc. Os
nomes SAMET e SELFT podem ocorrer numa interface secreta. A formalização é
ΛSAMET. ΛSELFT. priv(ϒc).
SINTERFACE(ϒc):
OBJTYPE(ϒ c):
Representa a faceta externa do tipo dos objectos gerados por uma classe com
interface externa INTERFACE(ϒc). Tal como em L5, trata-se do seguinte tipo-objecto recursivo
na variável SAMET: µSAMET.INTERFACE(ϒc)[SAMET]. Note que este tipo-objecto pode ser definido antes do tipo-objecto interno associado à mesma classe estar definido.
IOBJTYPE(ϒ c):
Representa a faceta interna do tipo dos objectos gerados por uma classe
com interface interna IINTERFACE(ϒ c). É um tipo-objecto recursivo na variável SELFT. No seu
contexto, o nome SAMET está predefinido como OBJTYPE(ϒc). A formalização do tipo-objecto
interno é portanto: µSELFT.IINTERFACE(ϒc)[OBJTYPE(ϒc)][SELFT].
CLASSTYPE(ϒc):
Representa o tipo-classe associado a uma classe com interface interna
IINTERFACE(ϒc) e interface externa INTERFACE(ϒc). Na sua formalização há que considerar a
introdução de várias entidades: o tipo aberto SAMET, o tipo aberto SELTF, uma função de coerção chamada hide com o tipo SELFT→SAMET, e ainda o nome self . Como a herança envolve
tanto componentes públicas como componentes privadas, as classes são tratadas como entidades que revelam todas as suas componentes. A ocultação da parte privada dos objectos gerados
é efectuada só depois da criação dos objectos, por meio da coerção hide.
O resto desta secção é quase todo dedicado à pormenorização dos detalhes da formalização
do tipo-classe CLASSTYPE(ϒc).
Começamos por descrever como o nome SAMET é introduzido. Tal como em L5, este nome
é introduzido como um tipo-objecto externo genérico usando ∀ SAMET≤* INTERFACE(ϒ c). Quantificado desta forma, SAMET representa todos os tipos-objecto externos gerados pelas subclasses duma classe com tipo CLASSTYPE(ϒc).
O nome SELFT é introduzido no contexto da quantificação de SAMET, como um tipo-objecto interno genérico usando a quantificação ∀SELFT≤* IINTERFACE(ϒc)[SAMET]. Sob esta quantificação, SELFT representa todos os tipos-objecto internos gerados pelas subclasses duma classe
com tipo CLASSTYPE(ϒc).
Seguidamente, o tipo-classe CLASSTYPE(ϒc) está parametrizado por uma coerção do tipo
SELFT→SAMET que se destina a ser aplicada sempre que um termo do tipo SELFT, por exemplo
self, seja usado num contexto onde um termo do tipo SAMET seja esperado.
Finalmente, como sabemos, toda a classe tem de estar parametrizada em função do significado de self. Assim, o último parâmetro de CLASSTYPE(ϒc) será SELFT , i.e. o tipo que atribuímos a self .
Uma classe do tipo CLASSTYPE(ϒc) gera objectos do tipo externo IOBJTYPE(ϒc). Mas, devido
ao mecanismo da herança, seria prematuro atribuir este tipo aos resultados de CLASSTYPE(ϒc).
Na realidade, temos de nos preocupar, não só com o tipo dos objectos gerados pela classe cor-
122
OM – Uma linguagem de programação multiparadigma
rente, mas também com o tipo dos objectos gerados pelas suas subclasses. Vamos escolher para tipo dos resultados IINTERFACE(ϒ c)[SAMET][SELFT], onde SAMET e SELFT estão quantificados da forma anteriormente apresentada. Para confirmar a justeza desta escolha verifiquemos o
caso particular dos objectos gerados pela classe corrente. Tomando, SAMET≡OBJTYPE(ϒc) e
SELFT≡IOBJTYPE(ϒ c), obtemos:
IINTERFACE(ϒc)[SAMET][SELFT]
= IINTERFACE(ϒc)[OBJTYPE(ϒc)][IOBJTYPE(ϒ c)]
= IOBJTYPE(ϒc)
Juntando todos estes elementos, obtemos como codificação para CLASSTYPE(ϒc):
∀ SAMET≤ *INTERFACE(ϒ c).
∀ SELFT≤*IINTERFACE(ϒc)[SAMET].
(SELFT→SAMET)→
SELFT→
IINTERFACE(ϒc)[SAMET][SELFT]
Para terminar esta subsecção, mostramos seguidamente que o par de tipos IOBJTYPE(ϒc),
OBJTYPE(ϒc) se encontra na relação de subtipo.
Teorema 7.2.1-1 IOBJTYPE(ϒc) ≤ OBJTYPE(ϒc).
Prova: Por unfolding, estes dois tipos podem ser assim reescritos:
OBJTYPE(ϒc)
=ˆ µSAMET.INTERFACE(ϒ c)[SAMET]
= pub(ϒ c)[OBJTYPE(ϒc)/SAMET]
IOBJTYPE(ϒ c)
=ˆ µSELFT.IINTERFACE(ϒ c)[OBJTYPE(ϒc)][SELFT]
= all(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
Ora SELFT não ocorre em OBJTYPE(ϒc), pelo que OBJTYPE(ϒ c) pode ser vacuamente reescrito da seguinte forma:
OBJTYPE(ϒc) = pub(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
A asserção que se pretende provar fica assim:
all(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
≤ pub(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
Mas esta asserção resulta imediatamente da aplicação da regra [Sub {…}].
7.2.2 Semântica dos termos
Analisamos agora as definições dos termos de L7, começando pelas classes. Na subsecção
7.2.2.2 estabelecemos as regras de boa formação das subclasses em L7, e na subsecção 7.2.2.4
discutimos o nosso método de ocultação da parte privada dos objectos.
7 Componentes privadas e variáveis de instância
123
7.2.2.1 Semântica das classes
Na definição do termo class Rc, o gerador polimórfico aí introduzido generaliza de forma imediata o termo correspondente de L5. Este gerador, parametrizado relativamente a SAMET,
SELFT, hide e self, produz objectos com tipo-objecto interno e constituídos por todas as componentes existentes, privadas e públicas. Como é regra na definição de qualquer forma derivada
de F+, a definição de class R c faz implicitamente a validação do código da classe: se, porventura, o registo Rc estiver mal tipificado, então a classe class R c está mal tipificada.
O tratamento da herança é formalizado na equação do termo subclasse imediata class\s Rc a
qual generaliza a definição correspondente de L5. O gerador introduzido na equação está parametrizado em função dos nomes SAMET, SELFT, hide e self , especificamente introduzidos no
contexto da subclasse. Quanto ao código herdado da superclasse, este é adaptado ao contexto
da subclasse, aplicando S , o gerador da superclasse, aos nomes SAMET, SELFT, hide e self da
subclasse. Finalmente, o registo resultante, a que se chama super, é estendido com as componentes específicas da subclasse, produzindo-se finalmente super+Rc. Assim se cria um novo gerador por extensão dum gerador existente.
7.2.2.2 Boa formação das subclasses
Consideremos agora a questão da boa tipificação do gerador que codifica o termo class\s R c.
Neste caso, a questão que mais importa investigar é a da boa tipificação do termo que adapta
as componentes da superclasse ao contexto da subclasse:
super = S[SAMET][SELFT] hide self
Neste termo, o gerador polimórfico S correspondente à superclasse espera um tipo-objecto
SAMET que obedeça à condição SAMET≤ * INTERFACE(ϒ s ), e ainda um tipo-objecto SELFT que
obedeça à condição SELFT≤ * IINTERFACE(ϒc)[SAMET]. No entanto S é aplicado a tipos-objecto
SAMET e SELFT tais que SAMET≤ * INTERFACE(ϒ s ⊕ϒc) e SELFT≤ * IINTERFACE(ϒs ⊕ϒc)[SAMET].
Portanto, para que a aplicação de S esteja bem tipificada, é necessário que os tipos-registo ϒs ,
ϒc sejam tais que as seguintes condições fiquem garantidas:
SAMET≤* INTERFACE(ϒ s ⊕ϒc) ⇒ SAMET≤ * INTERFACE(ϒ s )
SAMET≤* INTERFACE(ϒ s ⊕ϒc) ⇒
(SELFT≤* IINTERFACE(ϒs ⊕ϒc)[SAMET] ⇒ SELFT≤ * IINTERFACE(ϒs )[SAMET])
Estas são portanto as condições mais fracas que garantem a boa tipificação de S . No entanto, veremos que estas condições podem ser substituídas com vantagem pelo seguinte par de
condições que, conjuntamente, são um pouco mais fortes (menos gerais) que as anteriores (cf.
teorema 7.2.2.2-3):
SAMET≤* INTERFACE(ϒ s ⊕ϒc) ⇒ SAMET≤ * INTERFACE(ϒ s )
SINTERFACE(ϒs ⊕ϒc)≤SINTERFACE(ϒs )
124
OM – Uma linguagem de programação multiparadigma
A primeira condição deste par é idêntica à condição que obtivemos em L5, em situação
idêntica. A segunda condição, está expressa usando a relação de subtipo entre operadores de
tipos duplamente parametrizados (cf. regra [Sub Λ ]), sendo por isso particularmente simples de
usar e de validar. Aparentemente, ela sofre da desvantagem de bloquear todas as decisões de
tipificação (aberta ou fechada) tomadas na parte privada das classes, tanto relativamente a
SAMET como a SELFT . Felizmente isso não é problema: como aprendemos na secção 7.1.5, as
decisões de tipificação tomadas na parte privada das classes de L7 não afectam a capacidade
das suas subclasses gerarem subtipos. É assim, com agrado, que dispensamos a utilização da
complicada condição original.
Conjugando as duas condições anteriores, a primeira definida sobre interfaces externas e a
segunda definida sobre interfaces secretas, obtemos uma condição unificada sobre interfaces
globais a que chamaremos relação de extensão geral:
Definição 7.2.2.2-1 (Relação de extensão geral) Chamamos relação de extensão
geral, gen_ext, à relação binária entre interfaces globais que se define, por tradução para F+, da
seguinte forma:
GINTERFACE(ϒc) gen_ext GINTERFACE(ϒs ) =ˆ
(T≤ * INTERFACE(ϒ c) ⇒ T≤ * INTERFACE(ϒ s )) & SINTERFACE(ϒ c)≤SINTERFACE(ϒs )
Esta condição define uma relação binária no conjunto das interfaces globais, estabelecendo
quais são as modificações de interface que originam subclasses bem formadas. A relação é
reflexiva e transitiva (cf. teorema 7.2.2.2-2), o que é essencial para que a relação de subclasse
se mantenha reflexiva e transitiva em L7.
Sinteticamente, dizemos que, em L7, “herança implica extensibilidade geral” pois sem a
verificação da restrição de extensão geral entre interfaces globais, não seria possível ter
herança no modelo.
Note que a relação de extensão geral gen_ext degenera na relação de extensão ext de L5, no
caso de classes só com componentes públicas.
Teorema 7.2.2.2-2 A relação de extensão geral gen_ext é reflexiva e transitiva.
Prova: A demonstração é trivial. Sejam GAI =ˆ GINTERFACE(ϒA), GBI =ˆ GINTERFACE(ϒB),
GCI =ˆ GINTERFACE(ϒC). Sejam AI =ˆ INTERFACE(ϒ A), BI =ˆ INTERFACE(ϒ B), CI =ˆ INTERFACE(ϒ C).
Sejam SAI =ˆ SINTERFACE(ϒA), SBI =ˆ SINTERFACE(ϒB), SCI =ˆ SINTERFACE(ϒC).
A relação gen_ext é reflexiva pois a asserção
GAI gen_ext GAI
é equivalente à tautologia
(T≤ * AI ⇒ T≤* AI) & SAI≤SAI
Para verificarmos a transitividade vamos assumir:
7 Componentes privadas e variáveis de instância
125
GAI gen_ext GBI
IBI gen_ext GCI
ou seja:
(T≤ * AI ⇒ T≤* BI) & SAI≤SBI
(T≤ * BI ⇒ T≤* CI) & SBI≤SCI
Daqui, por transitividade de ⇒ e de ≤ obtemos:
(T≤ * AI ⇒ T≤* CI) & SAI≤SCI
ou seja:
GAI gen_ext GBI
Teorema 7.2.2.2-3 A relação de extensão geral gen_ext garante a boa formação das classes
em L7.
Prova: O lema 5.3-6 foi introduzido especificamente para simplificar esta demonstração, a
qual está longe de ser trivial. Considere as interfaces: I c =ˆ INTERFACE(ϒ c), IIc =ˆ IINTERFACE(ϒc),
SIc =ˆ SINTERFACE(ϒc), Is =ˆ INTERFACE(ϒ s ), IIs =ˆ IINTERFACE(ϒs ), SIs =ˆ SINTERFACE(ϒs ). Antes de
começarmos, note que a condição:
GNTERFACE(ϒc) gen_ext GNTERFACE(ϒs )
se reescreve, por definição, na condição:
SAMET≤* Ic ⇒ SAMET≤* Is & SI c≤SIs
[1]
e que também se reescreve, pelo lema 5.3-6, na condição:
SAMET≤* Ic ⇒ Ic[SAMET]≤Is [SAMET] & SIc≤SIs
[2]
Vamos provar que as condições de [1] ou [2] são suficientes para garantir as condições gerais de boa tipificação deduzidas no início da corrente subsecção. Recordamos que essas condições são as seguintes:
SAMET≤* Ic ⇒ SAMET≤* Is
SAMET≤* Ic ⇒
(SELFT≤* II c[SAMET] ⇒ SELFT≤* II s [SAMET])
A primeira destas duas condições já faz parte de [1]. Assim só precisamos de provar a
segunda.
Começamos, então, por assumir o antecedente SAMET≤* Ic. Note que a partir dele e da primeira parte de [2], se extrai imediatamente a conclusão:
Ic[SAMET]≤Is [SAMET]
[3]
Vamos agora partir de SELFT≤ * IIc[SAMET] para finalmente chegarmos a SELFT≤ * IIs [SAMET] :
126
OM – Uma linguagem de programação multiparadigma
SELFT≤* II c[SAMET]
⇒ SELFT≤* Ic[SAMET] & SELFT≤* SIc[SAMET] particionando a interface II c (em Ic e SIc)
⇒ SELFT≤* Is [SAMET] & SELFT≤* SIc[SAMET]
por [3] e [Sub trans]
*
*
⇒ SELFT≤ Is [SAMET] & SELFT≤ SIs [SAMET]
pela segunda parte de [2] e [Sub trans]
*
⇒ SELFT≤ II s [SAMET]
agrupando Is e SIs em IIs
7.2.2.3 Semântica dos outros termos
Nesta subsecção, explicamos a codificação dos três termos que permitem a criação de novos
objectos e o acesso a esses objectos.
Dentro duma classe c , o termo priv_new c permite criar objectos dessa mesma classe com
tipo interno IOBJTYPE(ϒc). Para isso, o gerador polimórfico C correspondente à classe c é instanciado com: (1) o tipo-objecto externo gerado pela classe; (2) o tipo-objecto interno gerado
pela classe; (3) uma função de coerção, chamada função de ocultação do tipo IOBJTYPE(ϒc)→
OBJTYPE(ϒc). Desta instanciação obtém-se um gerador monomórfico do tipo IOBJTYPE(ϒc)→
IOBJTYPE(ϒ c), ao qual se aplica o operador de ponto fixo, para se obter um novo objecto com
tipo interno.
O termo new c serve para criar a partir duma classe c , objectos dessa classe com tipo externo. Começa por criar um objecto com tipo interno, usando o termo priv_new c, e depois aplica
ao objecto resultante a função de ocultação descrita na secção seguinte.
O termo que permite o acesso às componentes de objectos com tipo externo ou tipo interno
– da forma o.l em ambos os casos –, traduz-se num simples acesso a uma componente dum registo.
Um comentário final sobre a necessidade de introduzir uma função de coerção na definição
geral de classe. Já vimos que o termo priv_new c instancia o gerador polimórfico C com os tipos
OBJTYPE(ϒc) e IOBJTYPE(ϒc). Ora este facto garante imediatamente que no interior dos objectos
gerados se irá verificar sempre a condição SELFT≤SAMET (cf. teorema 7.2.1-1). Era óptimo que
fosse possível incorporar esta condição directamente na definição geral de classe: assim, ficaria elegantemente resolvido o problema da ocultação da parte privada dos objectos de tipo
SELFT (quando usados num contexto de tipo SAMET). Infelizmente não é possível fazer isso, já
que as variáveis SELFT e SAMET são introduzidas quantificadas e o sistema F + não prevê a acumulação de restrições sobre variáveis. Assim, tivemos de resolver o problema da ocultação
com a ajuda duma função de coerção.
7.2.2.4 Função de ocultação
A função de ocultação hide:IOBJTYPE(ϒc)→OBJTYPE(ϒc) que é introduzida nas equações dos termos new c e priv_new c serve para ocultar a parte privada dos objectos criados. No caso do
termo new c , a função de ocultação é aplicada imediatamente ao objecto, logo após a sua
7 Componentes privadas e variáveis de instância
127
criação. No caso do termo priv_new c, a função de ocultação é usada como argumento de instanciação da classe c, para aplicação diferida ao objecto criado.
Definimos a função de ocultação de forma simples, com base em duas propriedades da linguagem L7: (1) a propriedade da perda de informação, descrita na secção 4.3.2; (2) a propriedade de IOBJTYPE(ϒc) ser subtipo de OBJTYPE(ϒc). Concretamente, para ocultar a parte privada
dum objecto do tipo IOBJTYPE(ϒc), fazemos a promoção do seu tipo interno ao supertipo
OBJTYPE(ϒc); o conservadorismo do sistema de tipos estático encarrega-se então de garantir a
privacidade das componentes secretas do objecto. Eis a definição da função de ocultação:
hide
=ˆ λx:IOBJTYPE(ϒc).((λy:OBJTYPE(ϒc).y) x)
No entanto, note que existe um potencial problema nesta abordagem. Trata-se da existência
da operação dinâmica de despromoção de tipo, introduzida na secção 4.3.5. Essa operação foi
inventada, exactamente para ultrapassar o problema da perda de informação (cf. secção 4.3.2).
A solução para propomos para impedir a recuperação do tipo interno de objectos com tipo
público é simples: introduzimos a seguinte restrição sobre a utilização da operação de despromoção de tipo.
Restrição 7.2.2.4-1 A operação downcast[τ] só podem ser aplicada a tipos externos.
Curiosamente, esta restrição só tem aplicação real no caso particular do tipo interno SELFT.
Todos os outros tipos internos são tipos técnicos (cf. secção 7.1.3), e já estavam proibidos de
ser explicitamente usados nos programas, em particular como argumentos de downcast.
Uma forma alternativa de definir a função de ocultação é a seguinte:
hide
=ˆ λx:IOBJTYPE(ϒc).+[OBJTYPE(ϒc)‚{}](x, {})
(concatenação de registos cf. secção 2.5.5)
= λx:IOBJTYPE(ϒc). {p1 =x.p1 ‚…‚p n =x.pn } (onde p1 …p n são as componentes públicas de x)
Esta função toma um objecto x com tipo interno, e cria um objecto novo (um novo registo) só
com métodos públicos, que se limita a ser uma porta de acesso indirecto e controlado a x. O
novo objecto está definido de tal forma que todas as mensagens enviadas para ele são redireccionadas para o objecto original x. Além disso as variáveis de instância do novo objecto são as
variáveis de instância públicas de x (i.e. existe partilha).
Esta última técnica é uma adaptação a F+ da técnica usada por Bruce nas regras da semântica operacional da sua linguagem PolyTOIL [BSG95]. Sendo compatível com a nossa formulação da linguagem L7, esta técnica poderia também ter sido adoptada por nós.
7.3 A linguagem imperativa L7&
Nesta secção, apresentamos e formalizamos a linguagem L7&, uma variante imperativa da linguagem L7 que introduz variáveis de instância mutáveis. A gramática e a tabela de equações
semânticas de L7& são apresentadas no final do capítulo corrente.
128
OM – Uma linguagem de programação multiparadigma
+
A linguagem L7 & é formalizada sobre o sistema F&
(na secção 7.3.2). A nova linguagem
+
integra todos os ingredientes imperativos de F& e também adopta a estratégia de avaliação
+
call-by-value de F&
.
A secção corrente está organizada da seguinte forma: na subsecção 7.3.1 apresentamos as
ideias essenciais sobre variáveis de instância e alguns exemplos simples; na subsecção 7.3.2
formalizamos L7& tomando como ponto de partida a formalização existente para L7; finalmente, na subsecção 7.3.3 discutimos uma questão pertinente para L7&: a interacção entre os
tipos-referência que ocorrem nas interfaces das classes e o mecanismo de herança.
7.3.1 Variáveis de instância
Na linguagem L7&, variáveis de instância é a designação genérica que adoptamos para as
componentes dos objectos cujos tipos são tipos-referência, ou seja tipos da forma Ref τ (cf.
secção 2.5.6). O estado dum objecto é definido como a sequência de valores das suas variáveis
de instância.
A linguagem L7& suporta variáveis de instância privadas e públicas. Isso significa que o
estado dum objecto pode ser parcialmente privado e parcialmente público (como em C++ e ao
contrário do Smalltalk).
Usando L7& de forma idiomática, é possível definir variáveis de instância semipúblicas, ou
seja, variáveis que são públicas para efeitos de leitura, mas privadas para efeitos de escrita.
Para definir uma variável de instância semipública, introduz-se primeiro uma variável de instância privada, e depois, à maneira de Reynolds, define-se um método público para leitura dessa variável. A vantagem das variáveis de instância semipúblicas é permitirem a um objecto
revelar partes do seu estado, sem perder controlo sobre este.
Convém dizer neste ponto que a nossa linguagem final, final, OM, suportará directamente
apenas variáveis de instância privadas e variáveis de instância semipúblicas. Naturalmente, será sempre possível simular variáveis de instância públicas, mas a sua utilização será desencorajada.
Para exemplificar a utilização de variáveis de instância privadas em L7&, exibimos uma variante da classe pointC (cf. secção 7.1.2), agora com duas variáveis de instância privadas, priv_x
e priv_y:
pointCR
=ˆ class { priv_x=ref (0:Nat), priv_y=ref (0:Nat),
sumx=λz:Unit. deref self.priv_x+deref self.priv_x,
priv_eq=λa:SELFT.(deref self.priv_x=deref a.priv_x
& deref self.priv_x=deref a.priv_x) }
Para exemplificar a utilização de variáveis de instância semipúblicas em L7&, exibimos
agora uma nova variante de pointC, contendo as variáveis de instância semipúblicas (simuladas) x e y:
7 Componentes privadas e variáveis de instância
pointCRsp
129
=ˆ class { priv_x=ref (0:Nat), priv_y=ref (0:Nat),
x=λz:Unit. deref self.priv_x,
y=λz:Unit. deref self.priv_y,
sum=λz:Unit. self.x ()+self.y (),
eq=λa:SAMET.(self.x ()=a.x () & self.y ()=a.y ()) }
Note como foi possível tornar público o método binário eq, agora que a classe passou a revelar
o estado dos seus objectos.
7.3.2 Semântica de L7&
Nesta secção discutimos os problemas envolvidos na adaptação da formalização de L7 ao caso
+
da linguagem L7&. As novas equações semânticas, definidas sobre F&
, encontram-se agrupadas na tabela colocada no final do presente capítulo.
Foram quatro os problemas com que nos confrontámos na adaptação das equações de L7.
Vamos dedicar uma subsecção a cada um desses problemas:
• 7.3.2.1: Tratamento dos pontos fixos no contexto da estratégia de avaliação call-by-value;
• 7.3.2.2: Determinação do local exacto das equações semânticas onde as variáveis de
instância devem ser criadas;
• 7.3.2.3: Permitir a ocorrência de self nas expressões de inicialização das variáveis de
instância;
• 7.3.2.4: Introdução duma constante polimórfica nil , compatível com todos os tipos-registo.
7.3.2.1 Tratamento dos pontos fixos
+
No sistema F&
o operador de ponto fixo só pode ser aplicado a funções cujos tipos tenham a
+
forma (υ→τ)→(υ→τ). Esta é uma consequência de F&
usar a estratégia de avaliação call-by-value (cf. secção 2.5.6).
Assim, quando se transita da semântica de L7 para a semântica de L7& é preciso reformu+
lar, nas equações semânticas, todas as funções de F&
que sejam alvo da aplicação do operador
fix, por forma a que os tipos dessas funções passem a ter a forma (υ→τ)→(υ→τ) . Duas destas
funções requerem a transformação referida. Trata-se dos dois geradores polimórficos, com tipo
CLASSTYPE(ϒ c), introduzidos nas equações semânticas das classes class R c e class\s R c. O tipo
desses geradores tem de mudar, por forma a que a sua parte mais interna deixe de ser:
SELFT→IINTERFACE(ϒc)[SAMET][SELFT]
para passar a ser:
(Unit→SELFT)→(Unit→IINTERFACE(ϒc)[SAMET][SELFT])
130
OM – Uma linguagem de programação multiparadigma
Paralelamente, a variável de recursão dos geradores, self , tem de deixar de ser do tipo SELFT e
passar a ser do tipo Unit→SELFT.
Esta transformação corresponde ao conhecido artifício técnico que permite simular a estratégia de avaliação call-by-name numa linguagem call-by-value.
Comparação com outros trabalhos - São escassos os estudos teóricos que lidam com objectos mutáveis. No trabalho [Pie93b], Pierce sugere o uso da técnica que descrevemos como forma de lidar com alguns dos problemas dos “objectos mutáveis”. Em [ESTZ94] apresenta-se
uma técnica alternativa baseada num operador de ponto fixo à Ladin, que tira partido das propriedades das referências. Este operador resolve duma só vez, não só o problema discutido
nesta secção, como também todos os problemas que discutiremos nas secções seguintes. Tem,
no entanto, a desvantagem de requerer uma codificação dos objectos muito complexa.
7.3.2.2 Criação das variáveis de instância
As referências que implementam as variáveis de instância dos objectos são criadas usando o
operador ref. Mas este operador tem o problema de funcionar por efeito lateral: sempre que
uma expressão ref exp é reavaliada, cria-se uma nova referência inicializada com o valor que a
expressão exp produziu.
Como se comenta no trabalho [ESTZ94], no momento da geração dum objecto, é muito fácil cair em equívocos relativamente ao momento exacto em que as suas variáveis de instância
devem ser criadas. Com efeito, não havendo cautela, estas podem ser criadas demasiado cedo,
ficando desta forma associadas à classe e não ao objecto gerado, ou podem ser criadas demasiado tarde, sendo neste caso recriadas sempre que se acede ao objecto.
Para que as variáveis de instância não sejam criadas demasiado cedo, as expressões onde
ocorre o operador ref devem ser reavaliadas sempre que a classe é alvo do operador new. Como
+
F&
usa a estratégia call-by-value, toda a classe deve ter pelo menos um parâmetro do qual dependam todos os usos de ref dentro da classe. Para isso poderíamos introduzir um parâmetro
artificial do tipo Unit; mas tal não é necessário visto que a classe já possui um parâmetro chamado hide.
Para que as variáveis de instância não sejam criadas demasiado tarde, o operador ref não
deve ser usado dentro do gerador monomórfico interno que será objecto da aplicação do operador de ponto fixo.
Portanto, o operador ref deve ocorrer depois da introdução do parâmetro hide, e antes do
gerador interno. Nas equações semânticas que escrevemos para as classes estas duas regras são
cumpridas. Nestas equações, confirme a localização das invocações de allocate, primitiva na
qual, por razões de organização, encapsulámos todos usos de ref.
Comparação com outros trabalhos - Os problemas aqui descritos são referidos de passagem
em [ESTZ94], embora nesse trabalho aquelas dificuldades não se coloquem em virtude do
7 Componentes privadas e variáveis de instância
131
operador de ponto fixo introduzido ser compatível com a estratégia de avaliação call-by-value
(trata-se do operador especial que já referimos na secção anterior).
O problema discutido nesta secção é propício à introdução de erros, pelo que resolvemos
testar a nossa semântica, escrevendo em Caml Light o seguinte protótipo para as equações semânticas das classes.
#let rec fix f x = f (fix f) x ;;
(* definição de fix *)
fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
#type Counter = { i: int ref;
inc:unit->Counter;
get:unit->int } ;;
Type Counter defined.
#let CounterClass () =
(* () é um argumento artificial *)
let r = ref 0 in (* criação da ref depois do argumento e antes do gerador*)
fun self () ->
(* () é um argumento artificial *)
{ i = r;
inc = (fun () -> (self()).i := !((self()).i)+1; (self()));
get = (fun () -> !((self()).i)) } ;;
CounterClass : unit -> (unit -> Counter) -> unit -> Counter = <fun>
#let new cl = fix (cl ()) () ;;
new : (unit -> (unit -> 'a) -> unit -> 'a) -> 'a = <fun>
#let o = new CounterClass ;;
o : Counter = {i = ref 0; inc = <fun>; get = <fun>}
#o.inc() ;;
- : Counter = {i = ref 1; inc = <fun>; get = <fun>}
#o.inc() ;;
- : Counter = {i = ref 2; inc = <fun>; get = <fun>}
#o.get();;
- : int = 2
Caml Light [Ler96, Mau95] é uma implementação da linguagem CAML, a qual é, por sua vez,
uma linguagem funcional que tem a particularidade de usar a estratégia de avaliação call-by-value.
7.3.2.3 Inicialização das variáveis de instância
A inicialização das variáveis de instância dum objecto pode ser efectuada pelo próprio
operador ref no momento em que este cria essas variáveis, a não ser que queiramos permitir a
ocorrência de self nas expressões de inicialização. Se o desejarmos, então confrontamo-nos
com mais uma dificuldade: as questões técnicas da secção anterior levaram-nos a transferir a
criação das referências para um contexto anterior à introdução do nome self, e nesse contexto o
nome self é desconhecido.
Resolvemos este problema de forma simples. Tomamos as várias expressões de inicialização que ocorrem na classe e usamo-las todas para compor um método de inicialização, chamado priv_init , que adicionamos à classe. Este método será depois usado pelo operador priv_new
para inicializar os objectos criados. Note que, tal como qualquer outro método, o método
priv_init é interpretado num contexto onde o nome self é já conhecido.
Com a introdução do método privado priv_init, a definição dos tipos GINTERFACE(ϒc),
IINTERFACE(ϒc), e SINTERFACE(ϒ c) tem de ser ligeiramente alterada, pois todos estes tipos ganham uma componente suplementar.
132
OM – Uma linguagem de programação multiparadigma
Resta agora a questão, bastante mais complicada, da inicialização preliminar das referências no momento em que são criadas (ver primitiva allocate). Para isso precisamos de distinguir
em cada tipo primitivo τ suportado pela linguagem L7 & um elemento particular, digamos um
zero denotado por zero[τ], que possa ser usado na inicialização. É fácil encontrar um zero na
maioria dos tipos. Por exemplo, pode usar-se a seguinte definição recursiva:
zero[Nat] =ˆ 0
zero[Bool] =ˆ false
zero[υ→τ] =ˆ λx:υ.zero[τ]
zero[ ∀X≤ * I.τ] =ˆ ∀ X≤* I.zero[τ]
Um caso em que é complicado encontrar um zero é o caso dos tipos-registo. Este caso é tratado na secção seguinte.
Comparação com outros trabalhos - A generalidade dos modelos da literatura que lidam
com objectos mutáveis não permitem a ocorrência de self nas expressões de inicialização de
variáveis de instância. O trabalho [ESTZ94] é uma excepção e resolve o problema através do
operador de ponto fixo especial que já referimos anteriormente.
7.3.2.4 Constante nil
Na secção anterior, relativamente à questão da inicialização preliminar das referências, não
tratámos o caso das referências para tipos-registo: os tipos-registo não têm à partida um zero
natural que possa ser usado na inicialização provisória das referências correspondentes.
Por isso, vamos introduzir em L7& a constante nil que discutimos da secção 2.5.7. Isso
obriga-nos a passar a codificar os objectos usando novos registos, cujos tipos têm a forma
––
Unit→{l :τ}. Nas equações de L7&, os objectos criados usando priv_new passam assim a ter tipos
internos da forma Unit→IOBJTYPE(ϒc) e os objectos criados usando priv_new passam a ter tipos
externos da forma Unit→OBJTYPE(ϒc). As operações de acesso a componentes de objectos são
redefinidas.
Note que, no final, existem duas razões distintas para a ocorrência ubíqua de tipos da forma
Unit→…., nas equações de L7&: (1) a simulação de call-by-name, para ser possível usar fix ; (2)
a necessidade de dispor da constante nil.
7.3.3 Tipos-referência e herança
Considerando quaisquer ocorrências de tipos-referência na interface global duma classe, digamos as ocorrências sublinhadas em GINTERFACE({f:(Ref Nat)→Bool, priv_x:Ref {}) , essas ocorrências não podem ser objecto de qualquer modificação na interface global de qualquer subclasse.
Esta limitação está implícita nas equações semânticas de L7 (cf. relação gen_ext) e resulta do
facto dum tipo-referência não admitir subtipos não triviais (cf. 2.5.6.2).
7 Componentes privadas e variáveis de instância
133
Facto 7.3.3-1 As ocorrências de tipos da forma Ref τ mantêm-se invariantes nas interfaces
globais das subclasses.
Uma consequência deste facto é significativa: o tipo de todas as variáveis de instância não
pode ser mudado nas subclasses, ao contrário do que se passa com os métodos.
Uma outra questão prende-se com o tipo Ref SAMET. Este é um tipo legítimo. Contudo, por
uma razão pragmática, iremos banir o seu uso da parte pública das classes. A fonte do problema é a seguinte: a ocorrência de SAMET em Ref SAMET não tem polaridade. Por isso, se
Ref SAMET aparecesse na interface pública duma qualquer classe c as consequências seriam as
seguintes:
• As subclasses de c não gerariam subtipos úteis;
• Caso fosse necessário que gerassem subtipos, o operador “+ ” não teria o desejado efeito
sobre c, i.e. as subclasses de +c também não gerariam subtipos úteis.
Como fazemos questão que exista sempre uma solução para o problema da geração de subtipos úteis, vamos introduzir a seguinte restrição:
Restrição 7.3.3-2 O tipo Ref SAMET está proibido de ocorrer em interfaces públicas.
Felizmente que as consequências da introdução desta restrição são benignas em L7 &. Para
começar, a nossa restrição não afecta a possibilidade de definir variáveis semipúblicas de tipo
SAMET. Em segundo lugar, ela não impede a simulação de variáveis públicas do tipo SAMET
através da introdução de variáveis privadas do tipo SAMET e métodos públicos de leitura/escrita dessas variáveis (os métodos de leitura são do tipo Unit→SAMET, onde SAMET tem polaridade positiva; os métodos de escrita são do tipo SAMET→Unit, onde SAMET tem polaridade negativa). Em terceiro lugar, um parâmetro de tipo ref SAMET pode sempre ser substituído por dois
parâmetros, um de tipo SAMET e outro do tipo SAMET→Unit, ou então por um parâmetro e um
resultado, ambos de tipo SAMET. Finalmente, as ocorrências naturais de Ref SAMET em interfaces públicas são raras, especialmente no caso da linguagem OM final, onde todas as
variáveis de instância não-privadas serão semipúblicas. Como vimos, uma variável semipública do tipo SAMET não oferece problema.
7.4 Conclusões
Relativamente às questões do encapsulamento e do estado, a maioria dos modelos teóricos
descritos na literatura adoptam a filosofia da linguagem Smalltalk, quer seja na sua forma
imperativa original, quer seja em alguma variante funcional. Essa filosofia é a seguinte: (1) as
restrições de visibilidade estabelecem-se ao nível de cada objecto; (2) o estado dum objecto é
privado e todos os seus métodos são públicos. Nestes modelos as questões da visibilidade e do
estado são misturadas, sendo o tratamento unificado dos dois aspectos efectuado usando abs-
134
OM – Uma linguagem de programação multiparadigma
tracção procedimental (closures) ou abstracção de tipos (tipos existenciais) [Rey78]. Alguns
dos estudos mais importantes dentro desta linha são: [ACV96, BCP99, BFSG98, Bru94,
BSG95, ACV96, CHC90, CP89, Car88, ESTZ94, Kam88, KR93, PT94, Red88]. O ponto (1)
desta abordagem conduz necessariamente à separação dos conceitos de classe e de tipo-objecto
Ainda, relativamente às questões do encapsulamento e do estado, muitas de linguagens práticas e alguns estudos teóricos aderem às ideias da linguagem C++. Neste caso a filosofia é a
seguinte: (1) as restrições de visibilidade estabelecem-se ao nível da classe, a qual é vista
como um tipo abstracto de dados; (2) as componentes dos objectos podem ser públicas ou
privadas, independentemente de serem variáveis de instância ou métodos. Nestes modelos,
tipicamente, introduz-se primeiro uma noção de objecto elementar com estado mas sem encapsulamento; depois, usa-se abstracção de tipos (tipos existenciais) para erguer barreiras de
controlo de acesso ao estado desses objectos. Dois dos estudos mais importantes dentro desta
categoria são os trabalhos [FM96, OW97]. Seguem este modelo, linguagens como o C++,
Java, Pizza, Eiffel, etc. Note que o ponto (1) desta abordagem favorece a unificação entre os
conceitos de classe e de tipo-objecto.
A linguagens L7& não se enquadra inteiramente em nenhuma das categorias anteriores. É
certo que as regras de visibilidade de L7& são essencialmente as do Smalltalk, já que existe
uma barreira de encapsulamento envolvendo cada objecto. Não obstante, cria-se uma excepção
para os objectos que são gerados dentro da sua própria classe: a estes objectos atribuímos
inicialmente o tipo SELFT, ficando assim eles sujeitos às restrições de visibilidade da linguagem C++ durante a fase inicial das suas vidas. No nosso modelo, os objectos começam por ser
criados sem qualquer barreira de acesso, tal como nos modelos da abordagem que descrevemos em segundo lugar; depois usamos a propriedade da perda de informação para ocultar a
sua parte privada (aqui as técnicas referidas nas abordagens anteriores não são aplicáveis). Em
L7 os conceitos de classe e de tipo-objecto ficam separados, tal como em Smalltalk.
7 Componentes privadas e variáveis de instância
135
Sintaxe dos géneros,tipos e termos de L7&
Κ ::= ∗ | ∗⇒Κ
τυϕϒI::= Bool | Nat | υ→τ | X |
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′ |
Unit | Ref τ |
SAMET | CLASSTYPE(ϒ) | INTERFACE(ϒ) | OBJTYPE(ϒ) |
SELFT | GINTERFACE(ϒ) | IINTERFACE(ϒ) | SINTERFACE(ϒ) | IOBJTYPE(ϒ)
∀ X≤*INTERFACE(ϒ B).τ |
priv(ϒ) | pub(ϒ) | all(ϒ)
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
() | ref (e:υ) | deref e | e:=e′ | e;e′ |
self | super | class R | class\s R | new c | o.l | priv_new c |
checkType[τ] | downcastσ[τ] |
λX≤*INTERFACE(ϒ B).e | P[τ] |
priv(R) | pub(R) | all(R)
Semântica dos tipos
GINTERFACE(ϒ c) :∗⇒∗
=ˆ
interface global
ΛSAMET.ΛSELFT.(ϒ c⊕{priv_init:Unit→Unit})
IINTERFACE(ϒ c) :∗⇒∗
=ˆ
interface interna
ΛSAMET.ΛSELFT.all(ϒ c⊕{priv_init:Unit→Unit})
SINTERFACE(ϒ c) :∗⇒∗
=ˆ
(= GINTERFACE(ϒc))
interface secreta
ΛSAMET.ΛSELFT.priv(ϒ c⊕{priv_init:Unit→Unit})
CLASSTYPE(ϒ c) :∗
=ˆ
tipo-classe
∀ SAMET≤*INTERFACE(ϒ c).
∀ SELFT≤*IINTERFACE(ϒ c)[SAMET].
(SELFT→SAMET)→
(Unit→SELFT)→
(Unit→IINTERFACE(ϒc)[SAMET][SELFT])
Semântica dos termos
ˆ
class Rc :CLASSTYPE(ϒ c) =
*
λSAMET≤ INTERFACE(ϒ c).
λSELFT≤*INTERFACE(ϒ c)[SAMET].
λhide:SELFT→SAMET.
let Rrefs:ϒ c⊕{priv_init:Unit→Unit}=(allocate[ϒc] Rc).
λself:Unit→SELFT.
λz:Unit.all(Rrefs)
ˆ
class\s Rc :CLASSTYPE(ϒ s⊕ϒc) =
let S:CLASSTYPE(ϒ s) = s in
λSAMET≤*INTERFACE(ϒ s⊕ϒc).
λSELFT≤*IINTERFACE(ϒ s⊕ϒc)[SAMET].
λhide:SELFT→SAMET.
let Rrefs:ϒ c⊕{priv_init:Unit→Unit}=(allocate[ϒc] Rc).
λself:Unit→SELFT.
let super:Unit→IINTERFACE(ϒs)[SAMET][SELFT] =
(S[SAMET][SELFT] hide self) in
λz:Unit.(super ())+all(Rrefs)
*Restrição implícita: ϒ s,ϒ c devem ser tais que:
GINTERFACE(ϒ s⊕ϒc) gen_ext GINTERFACE(ϒ s)
136
OM – Uma linguagem de programação multiparadigma
priv_new c :Unit→IOBJTYPE(ϒc)
let C:CLASSTYPE(ϒ c) = c in
=ˆ
let hide:IOBJTYPE(ϒ c)→OBJTYPE(ϒc) = λx:IOBJTYPE(ϒ c).((λy:OBJTYPE(ϒ c).y) x) in
let gen:(Unit→IOBJTYPE(ϒc))→(Unit→IOBJTYPE(ϒc)) =
(C[OBJTYPE(ϒ c)][IOBJTYPE(ϒ c)] hide) in
let priv_o:Unit→IOBJTYPE(ϒc) = fix gen in
let dummy:Unit = (priv_o ()).priv_init () in
priv_o
ˆ
new c :Unit→OBJTYPE(ϒc) =
let C:CLASSTYPE(ϒ c) = c in
let priv_o = priv_new C in
let hide:IOBJTYPE(ϒ c)→OBJTYPE(ϒc) = λx:IOBJTYPE(ϒ c).((λy:OBJTYPE(ϒ c).y) x) in
let o:Unit→OBJTYPE(ϒc) = λz:Unit.hide (priv_o ()) in
o
ˆ
o.l :τ =
let R:Unit→OBJTYPE(ϒc) = o in
(R ()).l
ˆ
o.l :τ =
let R:Unit→IOBJTYPE(ϒc) = o in
(R ()).l
ˆ
nilOBJTYPE(ϒ ) =
c
λz:Unit.divergeOBJTYPE(ϒ ) : Unit→OBJTYPE(ϒc)
c
allocate[{a1:Ref υ1,…,am:Ref υm, b1:τ1,…,bn:τn}]{a1=ref x1,…,am=ref xm, b1=y1,…,bn=yn}
=ˆ
{a1=ref zero[υ1],…,am=ref zero[υm], b1=y1,…,bn=yn,
priv_init=λz:Unit.(self.υ1:=x1; …; self.υm:=xm)}
Notas: - os τi são tipos com a particularidade de não serem tipos-referência;
- o registo-resultado tem novas referências e um novo método priv_init;
- a expressão zero[υ] representa um elemento do tipo υ. Se υ for um
ˆ nilυ
tipo-objecto então zero[υ] =
8 Componentes de classe
137
Capítulo 8
Componentes de classe
Sintaxe dos géneros,tipos e termos de L8
Κ ::= ∗ | ∗⇒Κ
τυϕϒI::= Bool | Nat | υ→τ | X |
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′ |
SAMET | CLASSTYPE(ϒ) | INTERFACE(ϒ) | OBJTYPE(ϒ) |
SELFT | GINTERFACE(ϒ) | IINTERFACE(ϒ) | SINTERFACE(ϒ) | IOBJTYPE(ϒ)
#INTERFACE(ϒ) | #IINTERFACE(ϒ) | #SINTERFACE(ϒ) |
#OBJTYPE(ϒ) | #IOBJTYPE(ϒ) |
∀ X≤*INTERFACE(ϒ B).τ |
#priv(ϒ) | priv(ϒ) | #pub(ϒ) | pub(ϒ) | #all(ϒ) | all(ϒ)
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
self | super | class R | class\s R | new c | c#l | o.l |
SELFC | SUPERC |
checkType[τ] | downcastσ[τ] |
λX≤*INTERFACE(ϒ B).e | P[τ] |
#priv(R) | priv(R) | #pub(R) | pub(R) | #all(R) | all(R)
Tipos-registo parciais
ˆ tipo-registo das componentes de classe privadas
#priv(ϒ c) =
=ˆ tipo-registo das componentes de instância privadas
=ˆ tipo-registo das componentes de classe públicas
ˆ tipo-registo das componentes de instância públicas
pub(ϒ c) =
ˆ tipo-registo de todas as componentes de classe
#all(ϒ c) =
ˆ tipo-registo de todas as componentes de instância
all(ϒ c) =
priv(ϒ c)
#pub(ϒ c)
Semântica dos tipos
GINTERFACE(ϒ c) :∗⇒∗
=ˆ
interface global
ΛSAMET.ΛSELFT. ϒ c⊕{#priv_new:Unit→SELFT, #priv_gen:SELFT→SELFT}
ˆ
#IINTERFACE(ϒ c) :∗⇒∗ =
interface interna
ΛSAMET.ΛSELFT.#all(ϒ c)⊕{#priv_new:Unit→SELFT, #priv_gen:SELFT→SELFT}
#SINTERFACE(ϒ c) :∗⇒∗
=ˆ
ΛSAMET.ΛSELFT.#priv(ϒ c)⊕{#priv_new:Unit→SELFT, #priv_gen:SELFT→SELFT}
#INTERFACE(ϒ c) :∗⇒∗
=ˆ
interface externa
ΛSAMET.#pub(ϒ c)
#IOBJTYPE(ϒ c) :∗
=ˆ
tipo-meta-objecto interno
#IINTERFACE(ϒ c)[OBJTYPE(ϒ c)][IOBJTYPE(ϒ c)]
#OBJTYPE(ϒ c) :∗
=ˆ
tipo-meta-objecto externo
#INTERFACE(ϒ c)[OBJTYPE(ϒ c)]
*Verifica-se: #IOBJTYPE(ϒ c)≤#OBJTYPE(ϒ c)
(teorema 8.2.1-1)
138
OM – Uma linguagem de programação multiparadigma
CLASSTYPE(ϒ c) :∗
=ˆ
tipo-classe
∀ SAMET≤*INTERFACE(ϒ c).
∀ SELFT≤*IINTERFACE(ϒ c)[SAMET].
(SELFT→SAMET)→
#IINTERFACE(ϒ c)[SAMET][SELFT]→
#IINTERFACE(ϒ c)[SAMET][SELFT]
Semântica das relações
Relação binária de extensão geral de L8 entre interfaces globais
ˆ
GINTERFACE(ϒ c) gen_ext2 GINTERFACE(ϒ s) =
*
*
(T≤ INTERFACE(ϒ c) ⇒ T≤ INTERFACE(ϒ s))
& SINTERFACE(ϒ c)≤SINTERFACE(ϒ s)
& #IINTERFACE(ϒ c)≤#IINTERFACE(ϒ s)
Registos parciais
ˆ registo das componentes de classe privadas
#priv(Rc) =
=ˆ registo das componentes de instância privadas
ˆ registo das componentes de classe públicas
#pub(Rc) =
ˆ registo das componentes de instância públicas
pub(Rc) =
ˆ registo de todas as componentes de classe
all(Rc) =
ˆ registo de todas as componentes de instância
#all(Rc) =
priv(Rc)
Semântica dos termos
ˆ
class{Rc} :CLASSTYPE(ϒ c) =
*
λSAMET≤ INTERFACE(ϒ c).
λSELFT≤*IINTERFACE(ϒ c)[SAMET].
λhide:SELFT→SAMET.
λSELFC:#IINTERFACE(ϒ c)[SAMET][SELFT].
λSAMEC:#IINTERFACE(ϒ c)[SAMET][SELFT].
#all(Rc)
+ {#priv_new = λz:Unit.(fix (SELFC.#priv_gen))}
+ {#priv_gen = λself:SELFT.all(Rc)}
class\s {Rc} :CLASSTYPE(ϒ s⊕ϒc)
=ˆ
let S:CLASSTYPE(ϒ s) = s in
λSAMET≤*INTERFACE(ϒ s⊕ϒc).
λSELFT≤*IINTERFACE(ϒ s⊕ϒc)[SAMET].
λhide:SELFT→SAMET.
λSELFC:#IINTERFACE(ϒ s⊕ϒc)[SAMET][SELFT].
let SUPERC:#IINTERFACE(ϒ s)[SAMET][SELFT] =
(S[SAMET][SELFT] hide SELFC) in
SUPERC
+ #all(Rc)
+ {#priv_new = λz:Unit.(fix (SELFC.#priv_gen))}
+ {#priv_gen =
λself:SELFT.
let super:SELFT=(SUPERC.#priv_gen self) in
super+all(Rc)}
*Restrição implícita: ϒ s,ϒ c devem ser tais que:
GINTERFACE(ϒ s⊕ϒc) gen_ext2 GINTERFACE(ϒ s)
8 Componentes de classe
new c :#OBJTYPE(ϒ c)
139
=ˆ
let C:CLASSTYPE(ϒ c) = c in
let hide:IOBJTYPE(ϒ c)→OBJTYPE(ϒc) = λx:IOBJTYPE(ϒ c).((λy:OBJTYPE(ϒ c).y) x) in
let #gen:#IOBJTYPE(ϒ c)→#IOBJTYPE(ϒc) = (C[OBJTYPE(ϒ c)][IOBJTYPE(ϒ c)] hide) in
let #priv_o:#IOBJTYPE(ϒ c) = fix #gen in
let #hide:#IOBJTYPE(ϒ c)→#OBJTYPE(ϒc) =
λx:#IOBJTYPE(ϒ c).((λy:#OBJTYPE(ϒ c).y)) x in
let #o:#OBJTYPE(ϒ c) = #hide #priv_o in
#o
mo#l :τ =
ˆ
let R:#OBJTYPE(ϒ c) = mo in
R.#l
o.l :τ =
ˆ
let R:OBJTYPE(ϒ c) = o in
R.l
Na secção 8.1 deste capítulo, introduzimos e discutimos informalmente os conceitos específicos da linguagem L8. Depois, na secção 8.2, formalizamos a semântica de L8. Na secção 8.3
comentamos alguns aspectos gerais da linguagem e introduzimos uma forma idiomática de
tirar partido do polimorfismo paramétrico em L8 que designaremos por polimorfismo de
classe. Finalmente, na secção 8.4, apresentamos algumas conclusões.
8.1 Conceitos e mecanismos de L8
Na linguagem L8, introduzimos suporte para componentes de classe e para classes recursivas.
Nas classes recursivas, os acessos recursivos às próprias classes serão efectuados usando os
nomes ligados SELFC e SUPERC.
As componentes de classe constituem um mecanismo de grande utilidade geral: permitem
definir construtores, variáveis partilhadas, e ainda exprimir informação logicamente associada
à classe. As componentes de classe serão também muito exploradas pela faceta estática do mecanismo dos modos (cf. secções 9.1.1 e 9.1.2.2). Na linguagem L8 existe suporte para componentes de classe privadas e componentes de classe públicas.
A existência de componentes de classe permite a introdução duma útil forma de polimorfismo paramétrico – chamada polimorfismo de classe – que se baseia na passagem simultânea
duma classe não-paramétrica e do tipo-objecto por esta gerado (cf. secção 8.3).
A introdução de classes recursivas em L8 corresponde a uma necessidade prática efectiva.
De facto, um método de classe deve ter acesso a qualquer componente de classe da sua própria
classe. Além disso, qualquer instância também deve poder aceder às componentes de classe da
sua própria classe.
A questão do estado mutável já foi tratada na secção 7.3 do capítulo anterior. Relativamente a esta questão, iremos assumir, nas discussões informais e nos exemplos, que a linguagem
L8 suporta variáveis de instância e variáveis de classe mutáveis, e ainda que usa a estratégia da
140
OM – Uma linguagem de programação multiparadigma
avaliação call-by-value. No entanto, ignoraremos esta dimensão da linguagem ao nível da sua
formalização. São duas as razões que estão na base desta nossa decisão: em primeiro lugar,
não gostaríamos de obscurecer as equações semânticas de L8 com excessivos tecnicismos; em
segundo lugar, não aprenderíamos nada de novo com tal formalização pois ela seria feita usando as técnicas já conhecidas da secção 7.3.
Três linguagens práticas que suportam, sob alguma forma, componentes de classe e classes
recursivas, são as linguagens Smalltalk, C++ e Java.
8.1.1 Componentes de classe e meta-objectos
Na linguagem L8, introduzimos um novo tipo de componentes nas classes: as componentes de
classe. Do ponto de vista lógico, estas componentes pertencem a própria classe e não às suas
instâncias. Para materializar essa associação, definimos para cada classe um objecto especial,
chamado meta-objecto, que integra todas as componentes de classe da classe respectiva.
Componentes de instância é a designação que adoptamos de agora em diante relativamente
às componentes específicas dos objectos normais. Um objecto normal, ou instância, é um
objecto que não é meta-objecto.
Em L8, cada classe descreve duas categorias de objectos em simultâneo: objectos normais
(instâncias) e meta-objectos. Os objectos normais são idênticos aos objectos de L7: eles contêm todas as componentes de instância definidas na sua classe-mãe e ainda as componentes de
instância herdadas. Já os meta-objectos contêm: as componentes de classe definidas na sua
classe-mãe; as componentes de classe herdadas; e ainda um método de classe primitivo privado, chamado #priv_new, que é automaticamente adicionado ao meta-objecto e serve para construir objectos normais. Através do respectivo construtor privado, #priv_new, cada meta-objecto
ganha o direito exclusivo de criar instâncias da sua classe. Isso explica a parte “meta-” da designação “meta-objecto”.
Sendo privado o construtor #priv_new, à partida um objecto normal só poderá ser criado dentro dos estritos limites sintácticos da sua classe-mãe, usando a notação (SELFC#priv_new ()) . Mas
nada nos impede de adicionar a uma classe construtores públicos programados à custa de
#priv_new. Se uma classe definir um tal construtor público, digamos #new , então passa a ser possível criar instâncias dessa classe fora dos limites sintácticos da classe. Para criar uma tal instância escreve-se (mo#new ()), onde mo representa o meta-objecto associado à classe.
8.1.2 Utilidade das componentes de classe
A utilidade mais imediata das componentes de classe é a possibilidade de definir múltiplos
métodos geradores de objectos – construtores – dentro duma classe. Por exemplo, imagine
uma classe que define pontos num espaço real a duas dimensões: faz sentido definir nessa
classe um construtor de pontos que aceite coordenadas cartesianas; um outro que aceite coor-
8 Componentes de classe
141
denadas polares; um outro que aceite um número complexo; ainda, um outro sem argumentos
que crie pontos inicializados duma forma predeterminada. Tipicamente, no corpo de todos
esses construtores começa-se por criar um novo objecto usando a expressão
(SELFC#priv_new ()) , depois reinicializa-se o novo objecto (que tem tipo SELFT) atribuindo
valores às suas variáveis de instância, privadas e públicas, e finalmente retorna-se o novo
objecto. Note que só faz sentido definir construtores em linguagens que suportem objectos
mutáveis, visto que a operação de reinicialização é uma operação que pressupõe que o objecto
tem estado mutável.
As componentes de classe são também úteis para representar informação que esteja logicamente associada à classe e não às suas instâncias. Por exemplo se quisermos que uma classe
distinga um objecto particular do seu domínio, digamos o zero dessa classe, então devemos
adicionar à classe uma constante pública (um método de classe público sem argumentos), com
nome #zero, por exemplo, e inicializada com o objecto pretendido.
As componentes de classe ajudam ainda a organizar as entidades globais. nos programas.
Entidades globais usadas numa única classe podem ser programadas localmente como componentes de classe privadas. Entidades globais usadas em várias classes podem ser programadas
como componentes de classe públicas, tirando-se assim partido das fronteiras das classes para
particionar o espaço de nomes globais do programa. Note que uma classe só com componentes
de classe degenera num módulo (embora se trate dum módulo especial, com capacidade de
herdar componentes de outros módulos).
8.1.3 Nomeação das componentes das classes
Necessitamos de estender as convenções de nomeação das componentes das classes, inicialmente introduzidas na secção 7.1.2. As convenções de L8 são as seguintes:
•
•
•
•
os nomes das componentes de classe privadas são prefixados por “#priv_”;
os nomes das componentes de instância privadas são prefixados com “ priv_”;
os nomes das componentes de classe públicas são prefixados por “#” mas não por “#priv_”;
os nomes das componentes de instância públicas não são prefixados por “#” nem “priv_”.
Por vezes teremos a necessidade de referenciar, alguns dos tipos-registo parciais que esta
convenção determina. Dado um tipo-registo ϒ, introduzimos as seguintes definições:
#priv(ϒ)
priv(ϒ)
#pub(ϒ)
pub(ϒ)
#all(ϒ)
all(ϒ)
– representa o tipo-registo parcial das componentes de classe privadas;
– representa o tipo-registo parcial das componentes de instância privadas;
– representa o tipo-registo parcial das componentes de classe públicas;
– representa o tipo-registo parcial das componentes de instância públicas;
– representa o tipo-registo parcial de todas as componentes de classe;
– representa o tipo-registo parcial de todas as componentes de instância.
142
OM – Uma linguagem de programação multiparadigma
Temos assim que, para um tipo-registo ϒ qualquer, as seguintes equivalências de tipos são
válidas:
ϒ=
#all(ϒ) =
all(ϒ) =
ϒ=
#priv(ϒ) ⊕ priv(ϒ) ⊕ #pub(ϒ) ⊕ pub(ϒ)
#priv(ϒ) ⊕ #pub(ϒ)
priv(ϒ) ⊕ pub(ϒ)
#all(ϒ) ⊕ all(ϒ)
Relativamente aos registos-valores, adoptamos convenções idênticas às dos tipos-registo.
Assim, dado um registo R, definimos os seguintes registos parciais analogamente: #priv(R),
priv(R), #pub(R), pub(R), #all(R) e all(R). As seguinte equivalência é válida para qualquer registo R
de L8:
R = #all(R) + all(R) = #priv(R) + priv(R) + #pub(R) + pub(R)
8.1.4 Tipos-objecto e tipos-meta-objecto
Tal como em L7, em L8 associamos dois tipos a cada objecto: um tipo-objecto externo,
OBJTYPE(ϒc), que representa a visão externa do objecto e expõe as suas componentes públicas
apenas, e um tipo-objecto interno, IOBJTYPE(ϒc), que representa a visão interna do objecto e
expõe as suas componentes públicas e privadas. A definição destes tipos-objecto mantém-se
inalterada em L8. Também se mantém a propriedade: OBJTYPE(ϒc)≤IOBJTYPE(ϒc).
Mas em L8 precisamos também de lidar com as questões de visibilidade dos meta-objectos.
Por isso associamos dois tipos a cada meta-objecto: um tipo-meta-objecto externo, denotado
#OBJTYPE(ϒc), que representa a visão externa do meta-objecto e expõe apenas as suas componentes públicas, e um tipo-meta-objecto interno, denotado #IOBJTYPE(ϒc), que representa a visão interna do meta-objecto e expõe as suas componentes públicas e privadas. Estes dois tipos-meta-objectos cumprem a condição: #OBJTYPE(ϒc)≤#IOBJTYPE(ϒc) (cf. teorema 8.2.1-1).
Os tipos tipos-meta-objecto, internos e externos, são tipos técnicos, introduzidos por questões de formalização da linguagem e não podem ser explicitamente usados nos programas. Recordamos que os tipos-objecto internos também são técnicos. Portanto, das várias variantes de
tipos-objecto suportados em L8, apenas os tipos-objecto externos são tipos não-técnicos e podem ser explicitamente usados nos programas.
O facto de estabelecermos que as duas formas de tipo-meta-objecto são técnicos, equivale a
dizer que decidimos impedir os programadores de manipularem explicitamente meta-objectos
nos programas.
Em L8, os tipos-objecto OBJTYPE(ϒc) e IOBJTYPE(ϒc) continuarão a ser recursivos nas
variáveis SAMET e SELFT , respectivamente. No entanto os tipos-meta-objecto #OBJTYPE(ϒc) e
#IOBJTYPE(ϒ c) não serão tipos recursivos.
8 Componentes de classe
143
8.1.5 Interfaces de classe
Para tratar das questões relacionadas com a visibilidade de nomes em L8 necessitamos de introduzir interfaces de classe, constituídas só por componentes de classe, a par de interfaces de
instância, constituídas só por componentes de instância.
Em L8 associamos sete interfaces a cada classe. Se ϒc for o tipo-registo das componentes
duma classe, então as respectivas interfaces são assim definidas:
GINTERFACE(ϒc)
INTERFACE(ϒ c)
IINTERFACE(ϒc)
SINTERFACE(ϒc)
#INTERFACE(ϒ c)
#IINTERFACE(ϒc)
#SNTERFACE(ϒc)
– interface global, regista todas as componentes duma classe;
– interface externa de instância, regista as comps. de instância públicas;
– interface interna de instância, regista as componentes de instância;
– interface secreta de instância, regista as comps. de instância privadas;
– interface externa de classe, regista as componentes de classe públicas;
– interface interna de classe, regista as componentes de classe;
– interface secreta de classe, regista as componentes de classe privadas;
A interface global distingue-se das outras por incluir a assinatura de todas as componentes
duma classe, quer elas sejam de instância ou de classe, quer sejam privadas ou públicas. As
restantes interfaces são interfaces parciais e podem ser geradas a partir da interface global de
forma automática.
Na linguagem L7, cada classe tinha associadas três interfaces de instância e uma interface
global. Em L8 as três interfaces de instância mantêm-se, mas a interface global tem de ser reformulada face à existência das componentes de classe.
O nome SAMET pode ocorrer em toda e qualquer das sete interfaces duma classe. Já o nome
SELFT só pode ocorrer em cinco delas, ficando excluído das interfaces externas INTERFACE(ϒc)
e #INTERFACE(ϒc).
Vejamos qual é a relação entre as interfaces duma classe e os tipos-objecto gerados por essa
mesma classe. Uma classe com interface global GINTERFACE(ϒc) gera directamente meta-objectos com faceta interna de tipo #IOBJTYPE(ϒc) e faceta externa de tipo #OBJTYPE(ϒc), e gera
indirectamente instâncias com faceta interna de tipo IOBJTYPE(ϒc) e faceta externa de tipo
OBJTYPE(ϒc).
8.1.6 Recursividade das classes e SELFC
Em L8, as classes são definidas recursivamente sobre a variável de recursão SELFC, a qual fica
ligada à própria classe. Assim, dentro do meta-objecto associado a uma classe, o nome SELFC
desempenha o mesmo papel que self desempenha nos objectos normais.
Através do nome SELFC, um método de classe pode aceder a qualquer das componentes de
classe da sua classe. Em particular, pode invocar-se a si próprio. Também através de SELFC,
uma instância pode aceder a qualquer das componentes de classe da sua classe-mãe.
144
OM – Uma linguagem de programação multiparadigma
Nos métodos de classe herdados e nos métodos de instância herdados, o nome SELFC é
reinterpretado no contexto das subclasses que os acolhem. Na formalização, o nome SELFC
será tratado de forma semelhante a self em L4, no sentido em que lhe será atribuído um tipo-meta-objecto fixo, tal como fizemos relativamente a self na linguagem L4.
Para exemplificar, apresentamos seguidamente a classe pointClass, que define diversos métodos de classe e na qual se fazem várias referencias ao nome SELFC .
pointClass
=ˆ class{ #new=λx:Nat.λy:Nat. (let p=SELFC#priv_new () in (p.priv_x:=x; p.priv_y:=y; p))
#newOne=λz:Unit. (let p=SELFC#priv_new () in (p.priv_x:=1; p.priv_y:=1; p))
#zero=SELFC#priv_new ()
priv_x=ref (0:Nat), priv_y=ref (0:Nat),
priv_eq=λa:SELFT.(deref self.priv_x=deref a.priv_x
& deref self.priv_y=deref a.priv_y),
clone=λz:Unit. (SELFC#new (deref self.priv_x) (deref self.priv_y)),
sum=λz:Unit. deref self.priv_x+deref self.priv_y }
8.1.7 Componentes de classe e herança
Tal como as componentes de instância, também as componentes de classes são herdadas pelas
subclasses. Alias, tal não podia deixar de ser. Para verificar esta ideia, consideremos o método
de instância clone definido na classe pointClass da secção anterior e dependente do método de
classe #new, também definido na mesma classe. Devido a esta dependência é importante que
sempre que o primeiro seja herdado, o segundo acompanhe o primeiro. Caso contrário, clone
ficaria mal definido na subclasse.
Sendo as componentes de classe são herdadas, convém que elas também possuam a sua
própria versão do nome super. Por isso introduzimos o nome SUPERC nas subclasses. No contexto duma subclasse e através de SUPERC, os métodos de classe e de instância ganham acesso
às versões originais das componentes de classe que estão disponíveis na superclasse. Este
acesso pode ser útil se entretanto essas componentes tiverem sido redefinidas na subclasse.
8.1.8 Resumo dos nomes especiais
No contexto das classes de L8 estão disponíveis diversos nomes especiais predefinidos. Como
são em grande número importa exibi-los conjuntamente numa lista. Na lista que apresentamos
seguidamente, indicamos junto de a cada nome o contexto em que esse nome pode ser usado:
self
super
SELFC
SUPERC
SAMET
SELFT
– no corpo dum método de instância;
– numa subclasse, no corpo dum método de instância;
– no corpo dum método de instância ou dum método de classe;
– numa subclasse, no corpo dum método de instância ou dum método de classe;
– numa classe, sem restrições;
– numa classe, fora no cabeçalho das componentes públicas.
8 Componentes de classe
145
8.2 Semântica de L8
Nesta secção, formalizamos a semântica de L8. Pela razão apresentada na introdução da secção 8.1, iremos ignorar agora a questão do estado mutável. A tabela das equações semânticas
de L8 abre o presente capítulo.
Em L8, uma classe passa a ser vista como um gerador de meta-objectos polimórfico e extensível, que é activado apenas uma vez para gerar o meta-objecto associado à classe. Cada
meta-objecto de L8 tem no seu interior duas componentes primitivas: um gerador de instâncias
auxiliar, #priv_gen , e um construtor privado, #priv_new. O primeiro é usado tecnicamente na especificação de herança e não se destina a ser usado nos programas. O segundo permite criar
instâncias de classes, e, este sim, é para ser usado nos programas. Na sua definição, o construtor #priv_new limita-se a aplicar o operador de ponto fixo ao gerador #priv_gen.
Os meta-objectos de L8 têm uma funcionalidade próxima dos objectos de L4 porque neles
atribui-se um tipo fixo a SELFC, como se faz na linguagem L4 relativamente a self . Os meta-objectos de L8 têm as seguintes diferenças relativamente aos objectos de L4: (1) o seu tipo
não é recursivo; (2) estão parametrizados em função de SAMET e de SELFT em vez de self e
super ; (3) suportam componentes privadas.
Já os objectos-normais (instâncias) têm uma funcionalidade próxima dos objectos de L5,
porque neles se atribui um tipo aberto a self . Os objectos normais de L8 têm as seguintes diferenças relativamente aos objectos de L5: (1) os nomes SELFC e SUPERC estão disponíveis no
seu interior, juntamente com os nomes self e super; (2) suportam componentes privadas.
8.2.1 Semântica dos tipos
Neste ponto, discutimos a codificação dos diversos tipos necessários à formalização de L8.
Uma classe de L8 caracteriza duas espécies de objectos em simultâneo: instâncias e meta-objectos. Por isso temos de considerar interfaces e tipos-objecto específicos para cada um dos
dois casos.
Recordamos que a forma geral duma classe é class R c ou class\s R c, onde Rc representa o registo de todas as componentes da classe. No que segue, admitiremos que o registo Rc tem o tipo-registo ϒc.
Representa a interface global duma classe com componentes ϒc. Nesta
interface consideramos todas as componentes da classe. Os nomes SAMET e SELFT podem
ocorrer numa interface global. A formalização é ΛSAMET.Λ SELFT.ϒc.
GINTERFACE(ϒc):
INTERFACE(ϒc), IINTERFACE(ϒc), SINTERFACE(ϒc):
São respectivamente as interfaces externa, interna e secreta duma classe com componentes ϒc. Registam apenas componentes de
instância. Definimo-las como em L7.
146
OM – Uma linguagem de programação multiparadigma
OBJTYPE(ϒ c), IOBJTYPE(ϒc): São respectivamente o tipo externo e o tipo interno das instân-
cias duma classe com interface global GINTERFACE(ϒc). Definimo-las como em L7.
#INTERFACE(ϒc), #IINTERFACE(ϒc), #SINTERFACE(ϒ c):
São respectivamente as meta-interfaces externa, interna e secreta duma classe com componentes ϒ c. Registam apenas componentes de classe.
#OBJTYPE(ϒ c), #IOBJTYPE(ϒc):
São respectivamente o tipo externo e o tipo interno dos
meta-objectos gerados por uma classe que tenha interface global GINTERFACE(ϒc). Estes tipos
não são recursivos mas dependem dos tipos das instâncias da respectiva classe. Definimos o
primeiro como #INTERFACE(ϒc)[OBJTYPE(ϒc)]; definimos o segundo como #IINTERFACE(ϒc)[
OBJTYPE(ϒc)][IOBJTYPE(ϒ c)] . Note bem: os meta-objectos são valores recursivos com tipo não-recursivo.
CLASSTYPE(ϒc): Representa o
tipo-classe das classes com interface global GINTERFACE(ϒc).
Trata-se dum tipo-gerador de meta-objectos cuja parametrização em SAMET e SELFT imita a
parametrização correspondente usada no tipo-classe CLASSTYPE(ϒc) de L7. Como parâmetros
extra, temos ainda uma função de coerção do tipo SELFT→SAMET, e aquele que será o tipo de
SELFC. Escolhemos o tipo de SELFC e o tipo do resultado do gerador de meta-objectos através
do método usado em L4: ambos ficam com o tipo #IINTERFACE(ϒc)[SAMET][SELFT]. Juntando
tudo obtemos:
∀ SAMET≤ * INTERFACE(ϒ c).
∀ SELFT≤* IINTERFACE(ϒc)[SAMET].
(SELFT→SAMET)→
#IINTERFACE(ϒc)[SAMET][SELFT]→
#IINTERFACE(ϒc)[SAMET][SELFT]
Para definir a semântica do termo new c, mais adiante, precisamos de provar a seguinte relação entre tipos-meta-objecto:
Teorema 8.2.1-1 #IOBJTYPE(ϒc) ≤ #OBJTYPE(ϒc).
Prova: A demonstração é simples e quase idêntica à demonstração do teorema 7.2.1-1.
Reescrevemos tipos #IOBJTYPE(ϒc) e IOBJTYPE(ϒc) da seguinte forma:
#OBJTYPE(ϒc)
=ˆ #INTERFACE(ϒ c)[OBJTYPE(ϒc)]
= pub(ϒ c)[OBJTYPE(ϒc)/SAMET]
#IOBJTYPE(ϒ c)
=ˆ #IINTERFACE(ϒc)[OBJTYPE(ϒc)][IOBJTYPE(ϒ c)]
= all(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
Ora SELFT não ocorre em #OBJTYPE(ϒc), pelo que #OBJTYPE(ϒc) pode ser vacuamente reescrito da seguinte forma:
#OBJTYPE(ϒc) = pub(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
A asserção que se pretende provar ganha assim a forma:
8 Componentes de classe
147
all(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
≤ pub(ϒ c)[OBJTYPE(ϒc)/SAMET,IOBJTYPE(ϒc)/SELFT]
Mas esta asserção resulta imediatamente da aplicação da regra [Sub {…}].
8.2.2 Semântica dos termos
Vamos agora analisar, de forma breve, as definições dos termos de L8. A apresentação é feita
por referência a algumas das equações semânticas que se encontram na tabela que abre o presente capítulo.
8.2.2.1 Semântica das classes
Na equação que define a classe class Rc, o gerador polimórfico aí introduzido destina-se a gerar
meta-objectos de tipo-meta-objecto interno e constituídos por todas as componentes de classe
existentes na classe. A cada meta-objecto é ainda adicionado um gerador de instâncias auxiliar
#priv_gen e um construtor privado #priv_new.
A equação do termo subclasse imediata class\s Rc formaliza o tratamento da herança em L8.
A explicação que já foi feita relativamente à semântica deste termo nas linguagem L4, L5 e L7
torna redundante a maioria das explicações que pudéssemos agora produzir. Salientamos apenas os seguintes dois aspectos: (1) o gerador de instâncias da superclasse #priv_gen é adaptado
ao contexto da subclasse através da sua aplicação ao nome self introduzido na subclasse (da seguinte forma: super=(SUPERC.#priv_gen self)); (2) em L8, o termo que adapta as componentes de
classe da superclasse ao contexto da subclasse é:
SUPERC = S[SAMET][SELFT] hide SELFC
8.2.2.2 Boa formação das subclasses
As restrições de boa tipificação que adoptamos para o termo anterior estendem as restrições
usadas em L7:
SAMET≤* INTERFACE(ϒ s ⊕ϒc) ⇒ SAMET≤ * INTERFACE(ϒ s )
SINTERFACE(ϒs ⊕ϒc)≤SINTERFACE(ϒs )
#IINTERFACE(ϒs ⊕ϒc)≤#IINTERFACE(ϒ s )
Estas condições bloqueiam as decisões de tipificação, aberta ou fechada, tomadas nas componentes de instância privadas e nas componentes de classe privadas e públicas. Elas permitem rever apenas as decisões de tipificação tomadas relativamente a componentes de instância
públicas: mas note que não precisamos de mais pois em L8, do ponto de vista do programador,
a relação de subtipo envolve apenas tipos-objecto públicos.
O conjunto das três condições anteriores define uma relação binária no conjunto das interfaces globais, estabelecendo quais são as modificações de interface que dão origem a subclas-
148
OM – Uma linguagem de programação multiparadigma
ses bem formadas. A relação é reflexiva e transitiva (cf. teorema 8.2.2.2-2), o que é essencial
para que a relação de subclasse se mantenha reflexiva e transitiva em L8.
Definição 8.2.2.2-1 (Relação de extensão geral de L8) Chamamos relação de
extensão geral, gen_ext2, à relação binária entre interfaces globais que se define, por tradução
para F+, da seguinte forma:
GINTERFACE(ϒc) gen_ext GINTERFACE(ϒs ) =ˆ
(T≤ * INTERFACE(ϒ c) ⇒ T≤ * INTERFACE(ϒ s ))
& SINTERFACE(ϒc)≤SINTERFACE(ϒs )
& #IINTERFACE(ϒs ⊕ϒc)≤#IINTERFACE(ϒ s )
Teorema 8.2.2.2-2 A relação de extensão geral gen_ext2 é reflexiva e transitiva.
Prova: A demonstração é trivial e muito parecida com a demonstração do teorema 7.2.2.2-2.
8.2.2.3 Semântica dos outros termos
Em L8, o termo new c refere-se à criação de meta-objectos. De acordo com a respectiva equação semântica, o gerador polimórfico de meta-objectos que a classe c representa é instanciado
com os tipos externo e interno das instâncias a criar pelo gerador interno #priv_gen e ainda pela
função de coerção hide, que já foi explicada na secção 7.2.2.4. Desta instanciação resulta um
gerador monomórfico de meta-objectos do tipo #IOBJTYPE(ϒc)→#IOBJTYPE(ϒc), ao qual se aplica o operador de ponto fixo para estabelecer a ligação do nome SELFC dentro do meta-objecto
assim criado. Finalmente, ocultamos a parte privada do meta-objecto usando mais uma vez a
propriedade da perda de informação e tirando partido do teorema 8.2.1-1.
Quanto aos termos que descrevem o acesso às componentes dos meta-objectos e objectos –
termos mo#l e o.l –, estes traduzem-se para um simples acesso a uma componente dum registo.
8.3 Discussão sobre L8
Um aspecto interessante da hierarquia de classes de L8 é que ela também pode ser vista como
uma dupla hierarquia de classes de L7: uma hierarquia de classes geradoras objectos normais,
e uma hierarquia de classes geradoras meta-objectos. As duas hierarquias são paralelas no
sentido em que existe uma bijecção natural mútua. A interdependência entre cada par de
classes associadas é forte: as duas classes têm de ter privilégios totais de acesso mutuo às
partes privadas respectivas, e a classe que produz meta-objectos tem de estar parametrizada
sobre os tipos-objecto (interno e externo) gerados pela classe que produz instâncias.
Com a introdução das componentes de classe, surge em L8 uma interessante forma padronizada de tirar partido do polimorfismo paramétrico da linguagem, que será adoptada na linguagem OM final. Dedicamos a subsecção seguinte a esta questão.
8 Componentes de classe
149
8.3.1 Polimorfismo de classe
Já o referimos na secção 6.3.1, no contexto duma entidade paramétrica P =ˆ λX≤* I.e existe por
vezes a necessidade de ter à disposição um pacote de constantes e funções utilitárias com tipos
dependentes de X. Só por essa via se consegue resolver o problema da inicialização de variáveis locais do tipo X (usando alguma constante do tipo X ), ou o problema da criação de novos
objectos do tipo X.
Também já vimos que esta questão se resolve de forma simples, adicionando à entidade paramétrica P um segundo parâmetro, ops:OpsT[X], que torne o corpo de P dependente dum registo
de constantes e funções com as características pretendidas:
P
=ˆ λX≤* I.λops:OpsT[X].e
:∀X≤* I.OpsT[X]→τ
Esta questão sofre natural evolução em L8 pois, dado um tipo-objecto qualquer τ, qualquer
classe geradora de instâncias do tipo τ já tem associado um registo de constantes e funções sobre τ : referimo-nos ao meta-objecto associado a essa classe.
8.3.1.1 Definição de polimorfismo de classe
Em L8, torna-se particularmente natural introduzir um uso padronizado de polimorfismo paramétrico que, dada uma classe s, contemple a passagem de qualquer sua subclasse c acompanhada pelo respectivo tipo-objecto gerado τ c. Eis uma entidade paramétrica de L8 que captura
esta ideia:
λX T≤* INTERFACE(ϒ s ).λX M:#INTERFACE(ϒs )[XM].e
:∀X M≤* INTERFACE(ϒ s ).#INTERFACE(ϒs )[XT]→τ
Chamamos polimorfismo de classe a esta forma padronizada de polimorfismo que permite
a passagem simultânea duma classe e do respectivo tipo-objecto gerado. Na linguagem OM final, polimorfismo de classe será a única forma de polimorfismo paramétrico disponível.
Se pretendêssemos oficializar uma sintaxe para esta forma de polimorfismo, uma boa possibilidade seria a seguinte:
φX≤ * GINTERFACE(ϒc).e
=ˆ λX T≤* INTERFACE(ϒ c).λX M:#INTERFACE(ϒc)[XT].e
Do lado esquerdo, a interface-limite global GINTERFACE(ϒc) inclui apenas assinaturas de componentes de classe públicas e de componentes de instância públicas, o que permite impor simultaneamente restrições sobre um tipo parâmetro XT e um meta-objecto parâmetro XM. Convenciona-se que, no corpo da abstracção, a interpretação do nome X é dual: (1) nos contextos
da forma X#… (e.g. (X#new ()) ou X#zero ) o nome X representa a classe (ou meta-objecto) XM;
(2) nos restantes contextos, o nome X é interpretado como o tipo-objecto X T.
8.3.1.2 Boa tipificação da instanciação com variáveis de tipo
Refaçamos agora a análise da secção 6.2.2 para esta forma de polimorfismo paramétrico.
150
OM – Uma linguagem de programação multiparadigma
Consideremos as duas abstracções paramétricas S e C:
S =ˆ φY≤ * Gs .e
C =ˆ φX≤ * Gc.S[X]
Sob que condições é que a instanciação S[X] , efectuada dentro da abstracção C, está bem tipificada? Para começar, note que X tem uma interpretação dual em S[X] , devendo a notação S[X]
ser considerada como abreviatura deS[XT][XM]. Como, por definição, as interfaces globais G s e
Gc não contêm componentes privadas, uma condição suficiente que garante a boa tipificação
de S[X] é a seguinte:
Gc gen_ext2 Gs
Esta condição generaliza a condição descoberta na secção 6.2.2.
Em L8, para além de variáveis de tipo introduzidas nas abstracções paramétricas existe ainda a variável de tipo predefinida SAMET. Este é o caso que analisamos seguidamente.
Consideremos novamente a abstracção paramétrica S:
S =ˆ φY≤ * Gs .e
Sob que condições é que a instanciação S[SAMET] (que abrevia S[SAMET][SAMEC]), efectuada
dentro duma classe c , está bem tipificada? Vamos mostrar que é suficiente que a condição
GINTERFACE(ϒc) gen_ext2 Gs seja válida.
O nome SAMET é introduzido na classe c sujeito à restrição SAMET≤ * INTERFACE(ϒ c). Ora
partindo da condição GINTERFACE(ϒc) gen_ext2 Gs , prova-se imediatamente, pela definição de
gen_ext2, que SAMET≤ * INTERFACE(ϒ s ) e portanto SAMET pode ser usado como primeiro argumento de S .
Relativamente a SELFC, introduzido em c como SELFC:#IINTERFACE(ϒc)[SAMET][SELFT], a
terceira parte de gen_ext2 permite-nos concluir que SAMEC:#IINTERFACE(ϒ s )[SAMET][SELFT] e
portanto SAMET pode ser usado como segundo argumento de S .
8.4 Conclusões
A linguagem L8 suporta classes, objectos mutáveis com parte privada, polimorfismo paramétrico e componentes de classe. Comparando L8 com linguagens como o C++ ou o Java, verifica-se que L8 as supera em vários aspectos: tem sistema de tipos estático coordenado com um
um mecanismo de herança flexível; suporta polimorfismo paramétrico ao nível do sistema de
tipos (o C++ suporta apenas expansão textual de entidades paramétricas, o Java nem isso, embora a sua variante experimental Pizza [OW97] já disponha desse mecanismo); tem uma semântica bem definida, por redução à semântica do calculo-lambda F+.
Claro que o C++ e o Java superam a linguagem L8 noutros aspectos: os aspectos pragmáticos destas linguagens estão mais desenvolvidos; o C++ inclui suporte para herança múltipla e
8 Componentes de classe
151
para programação de sistemas; o Java dispõe dum útil mecanismo de modularidade – o package –, etc.
Em qualquer caso, podemos concluir que a linguagem L8, sem ser excessivamente complicada, já incorpora um conjunto suficientemente rico de mecanismos para ser bastante usável
na prática.
Na literatura não há muitas referências a modelos tipificados que estudem o mecanismo das
componentes de classe. Só conseguimos citar os trabalhos [CHC90] e [Bru94] que, mesmo
não estudando directamente esse mecanismo, tratam um aspecto parcelar de L8: a introdução
dum nome MyClass que permite que instâncias duma classe acedam à sua própria classe para
criar objectos irmãos. Na medida em que transformam as classes em entidades recursivas, os
nomes MyClass e SELFC são análogos entre si.
No entanto, no capítulo corrente, o nome SELFC, conjuntamente com SUPERC, foi realmente
alvo de tratamento mais alargado que abrangeu também a formalização das componentes de
classe. Se desejássemos introduzir apenas a construção MyClass , o contexto apropriado para o
fazer seria o contexto da linguagem L5: bastaria adicionar à equação semântica da classe o parâmetro extra MyClass: SAMET→SAMET, e introduzir na definição de new c uma aplicação suplementar de fix.
Capítulo 9
Modos
Sintaxe dos géneros,tipos e termos de L9
Κ ::= ∗ | ∗⇒Κ
τυϕϒI::= Bool | Nat | υ→τ | X |
ΛX.τ
– –
| ϕ[τ] | {l:τ} | ϒ⊕ϒ′ |
SAMET | CLASSTYPE(ϒ) | INTERFACE(ϒ) | OBJTYPE(ϒ) |
SELFT | GINTERFACE(ϒ) | IINTERFACE(ϒ) | SINTERFACE(ϒ) | IOBJTYPE(ϒ)
#INTERFACE(ϒ) | #IINTERFACE(ϒ) | #SINTERFACE(ϒ) |
#OBJTYPE(ϒ) | #IOBJTYPE(ϒ) |
∀ X≤*INTERFACE(ϒ B).τ |
MODEOP X.ϒ M | ϕ T
MODETYPE X≤*INTERFACE(ϒ B).ϒ | Μ τ
#priv(ϒ) | priv(ϒ) | #pub(ϒ) | pub(ϒ) | #all(ϒ) | all(ϒ)
– –
efcomRP::= lτ | θ τ | x | λx:υ.e | f e | rec x:τ.e | {l=e} | R.l |
self | super | class R | class\s R | new c | c#l | o.l |
SELFC | SUPERC |
checkType[τ] | downcastσ[τ] |
λX≤*INTERFACE(ϒ B).e | P[τ] |
mode X≤*INTERFACE(ϒ B).R | m τ |
#priv(R) | priv(R) | #pub(R) | pub(R) | #all(R) | all(R)
Semântica dos tipos
MODETYPE X≤*INTERFACE(ϒ B).ϒ M :∗
=ˆ
tipo-modo
∀ X≤*INTERFACE(ϒ B).
CLASSTYPE(pub(ϒ B)[X/SAMET]⊕ϒM)
onde ϒ M≤{$access:Unit→X}
(MODETYPE X≤*INTERFACE(ϒ B).ϒ M) T :∗
=ˆ
instanciação de tipo-modo
CLASSTYPE(ϒ T⊕ϒM[T/X])
MODEOP X.ϒ M :∗⇒∗
ΛX.OBJTYPE(ϒ M)
ˆ
ϕ T :∗ =
ϒ T⊕ϕ[T]
=ˆ
operador de modo (gerado por modo)
instanciação de operador de modo
154
OM – Uma linguagem de programação multiparadigma
Semântica dos termos
mode X≤*INTERFACE(ϒ B).RM :MODETYPE X≤*INTERFACE(ϒ B).ϒ M
λX≤*INTERFACE(ϒ B).
class{access[pub(ϒ B)[X/SAMET]]+RM}
=ˆ
modo
ˆ
m T :CLASSTYPE(ϒ T⊕ϒM[T/X]) =
let M:MODETYPE X≤*INTERFACE(ϒ B).ϒ M = m in
let S:CLASSTYPE(pub(ϒ B)[T/SAMET]⊕ϒM[T/X]) = M[T] in
λSAMET≤*INTERFACE(ϒ T⊕ϒM[T/X]).
λSELFT≤*IINTERFACE(ϒ T⊕ϒM[T/X])[SAMET].
λhide:SELFT→SAMET.
λSELFC:#IINTERFACE(ϒ T⊕ϒM[T/X])[SAMET][SELFT].
let _SUPERC:#IINTERFACE(pub(ϒ B)[T/SAMET]⊕ϒM[T/X])[SAMET][SELFT] =
(S[SAMET][SELFT] hide SELFC) in
_SUPERC
+ {#priv_new = λz:Unit.(fix (SELFC.#priv_gen))}
+ {#priv_gen = λself:SELFT.(access[ϒ T]
+ (_SUPERC.#priv_gen self))}
ˆ pub(ϒ)[T/SAMET]
onde ϒ T =
com T = OBJTYPE(ϒ) e OBJTYPE(ϒ)≤*INTERFACE(ϒ B)
ou T≤*INTERFACE(ϒ) e INTERFACE(ϒ) ext INTERFACE(ϒ B)
Macro auxiliar
– –
—————————
ˆ {l–=λz:Unit.(self.$access ()).l–}:{l–:Unit→τ
access[{l:τ}] =
}
Na secção 9.1, apresentamos e discutimos informalmente o mecanismo dos modos de L9. Na
secção 9.2, formalizamos a semântica o mecanismo dos modos. Na secção 9.3 referimos, com
brevidade, outros aspectos importantes de L9. Na secção 9.4 extraímos algumas conclusões.
9.1 Conceitos e mecanismos de L9
Na linguagem L9, introduzimos o mecanismo dos modos, um mecanismo de extensão semântica que constituirá a base das características multiparadigma da linguagem OM. O mecanismo
dos modos é o último mecanismo de natureza dinâmica que consideramos nesta tese.
9.1.1 Modos
O mecanismo dos modos é uma ferramenta de programação de nível meta que permite estender a semântica da linguagem L9 usando a própria linguagem. Actua influenciando a funcionalidade das entidades tipificadas da linguagem, ou seja, dos objectos, variáveis, expressões,
parâmetros de função e resultados de função.
Dependendo do modo duma entidade tipificada, digamos duma variável, as suas propriedades semânticas podem variar significativamente. Assim, por exemplo, uma variável com
modo lógico, log, tem a funcionalidade das variáveis simbólicas da linguagem Prolog [CM81,
Hog84]; uma variável com modo constante, const , é obrigatoriamente inicializada no ponto da
declaração e nunca mais pode ser alterada; uma variável sem modo tem a funcionalidade
primitiva das variáveis de tipo Ref τ (introduzidas linguagem L7&).
9 Modos
155
O mecanismo dos modos compreende uma faceta dinâmica e uma faceta estática:
• A faceta dinâmica concentra-se na questão da modificação ou do enriquecimento da
funcionalidade dos objectos com modo. Note que, tipicamente um objecto com modo
tem uma funcionalidade alterada relativamente a um objecto do mesmo tipo base mas
sem modo.
• A faceta estática concentra-se no controlo da funcionalidade das entidades estáticas
com modo. As entidades estáticas são as variáveis, expressões, parâmetros de funções e
resultados de funções.
As duas facetas dum modo definem-se conjuntamente numa construção sintáctica chamada
modo. Um modo tem uma estrutura semelhante a uma classe parametrizada de L8, e nele é
possível identificar uma parte dinâmica e uma parte estática:
• A parte dinâmica dum modo consiste na implementação dos objectos com esse modo.
Essa implementação é escrita usando a própria linguagem L9.
• A parte estática envolve o sistema de tipos da linguagem e questões de açúcar sintáctico. Na linguagem OM final, a parte estática dum modo será especificada usando os seguintes recursos:
- componentes específicas: elas determinam a interface global dum modo;
- métodos de coerção: estes métodos servem para indicar quais são as conversões de
modo legítimas que podem envolver o modo em questão (cf. secção 10.2.1);
- métodos “#$def_*”ou de sobreposição de sintaxe: estes métodos permitem forçar a
reinterpretação de certas construções sintácticas de OM, quando aplicadas a entidades com o modo em questão (cf. secção 11.3.1);
- globalização de métodos: é possível globalizar alguns métodos de classe específicos para com isso certos pequenos problemas técnicos discutidos na secção 11.5.
A faceta dinâmica será o tópico essencial do capítulo 9. A linguagem L9 suporta apenas a
faceta dinâmica dos modos. Quanto à faceta estática, esta requer recursos – coerções e sobreposição de sintaxe – que só a partir das linguagens L10 e OM ficarão disponíveis.
9.1.2 Exemplo: o modo log
Antes de tentarmos ser rigorosos relativamente a uma definição de modo, nesta secção e a título de ilustração, vamos discutir uma versão simplificada do modo de biblioteca log. A definição completa do modo log encontra-se na secção 12.5.
O modo lógico, log, introduz as ideias de variável lógica e unificação (sintáctica e semântica). São duas as principais operações específicas que ele suportada: isVar e '=='.
Ao contrário do que se poderia esperar, o modo log não suporta um mecanismo de retrocesso (backtracking). Essa é uma tarefa específica de outro modo, o modo gen ou modo dos geradores. O modo gen tira partido da noção de gerador para introduzir retrocesso na avaliação de
156
OM – Uma linguagem de programação multiparadigma
expressões, à maneira da linguagem Icon [Gri83]. Entre as componentes específicas do modo
gen incluem-se as primitivas: repeat, fail, ‘&’, '|', '~'.
O sistema de coerções da linguagem OM final permitirá que as componentes específicas do
modo gen possam ser aplicadas a entidades com modo log. Em cada uma dessas situações, o
sistema de coerções actua, promovendo as entidades lógicas envolvidas a entidades com o
modo composto gen log. Como as propriedades deste modo composto resultam da acumulação
das propriedades dos modos componentes, é desta forma indirecta que se introduz retrocesso
na avaliação de expressões lógicas.
9.1.2.1 Faceta dinâmica do modo log
Começamos por apresentar a faceta dinâmica do modo log. Esta ocupa-se exclusivamente da
implementação de objectos lógicos sobre um tipo τ, objectos especiais cuja funcionalidade se
aproxima da funcionalidade das variáveis simbólicas da linguagem Prolog.
A classe dum objecto lógico sobre um tipo τ escreve-se log τ . O tipo dum objecto lógico
sobre um tipo τ escreve-se LogT τ. Assumimos que apenas os objectos da classe log τ têm tipo
LogT τ. Esta identificação entre a classe log τ e o tipo LogT τ pode ser estabelecida usando a técnica introduzida na secção 4.3.6 (voltaremos a este assunto na secção 9.3).
Um objecto da classe log τ ,tem três estados possíveis: (1) não-ligado; (2) ligado a outro
objecto lógico da classe log τ; (3) ligado a um objecto do tipo τ. Usando objectos lógicos é possível exprimir restrições lógicas complexas, à semelhança do que se faz em muitas implementações da linguagem Prolog [Aït90, Dia90, Gab85, Her89, Hog84, KB85, War77, War83,
War88].
O modo log introduz diversas operações sobre objectos lógicos que conjuntamente se designam por componentes específicas do modo. Há duas operações particularmente importantes:
o método isVar que testa se um objecto lógico está ou não instanciado (i.e. se tem um valor), e
o método '==' que implementa uma operação de unificação.
Para além das componentes específicas do modo lógico, os objectos lógicos suportam ainda
todas as componentes públicas dos objectos de tipo τ, de forma automática. Quando uma dessas componentes é acedida num objecto lógico z, duas situações podem ocorrer: ou z está instanciado com um objecto, dito conexo, de tipo τ e, nesse caso, é a correspondente componente
do objecto conexo que acaba por ser acedida (digamos que z redirige aquele acesso para o objecto conexo); ou z não está instanciado e, neste caso, teria de ser gerada uma excepção (na
prática, para evitar a excepção o modo gen intervém para provocar falhanço e retrocesso: ver
definição do método #gen_from_log na secção 12.5).
Os objectos lógicos sobre τ são exemplos do que nós chamamos objectos de funcionalidade
enriquecida ou modificada. Sendo LogT τ o tipo dos objectos lógicos sobre τ , LogT τ inclui todas as componentes de τ e ainda as componentes específicas do modo log. No entanto, isso não
9 Modos
157
significa que tenhamos imediatamente LogT τ≤τ. Por exemplo, basta que τ tenha ocorrências
negativas de SAMET para que isso não aconteça, como sabemos do capítulo 5.
Para que seja possível definir a operação de unificação do modo lógico, qualquer tipo τ que
seja usado na instanciação deste modo tem de incluir a assinatura dum método de igualdade,
ou seja, deve verificar a condição: τ≤ * {'==':SAMET→Bool}. Em breve, veremos que os modos
constituem entidades paramétricas ≤* -restringidas. No caso do modo lógico, a interface-limite
que lhe está associada é {'==':SAMET→Bool}.
Uma nota final. Os objectos lógicos são introduzidos no modo lógico por puras razões de
implementação. A intenção é que eles passem desapercebidos ao programador que usa o modo
log. Apenas a sua influência sobre a semântica da linguagem deverá notada: por exemplo, para
o programador deverá ser óbvio que uma variável declarada com modo lógico fica com propriedades bem diferentes duma variável declarada com outro modo.
9.1.2.2 Faceta estática do modo log
Consideramos agora a faceta estática do modo log. A faceta estática do modo log especifica a
funcionalidade das entidades estáticas com modo log.
Começamos pela questão das coerções de modo, ilustrando-a através de dois exemplos:
Se um objecto com tipo simples, sem modo, τ for passado como argumento para um método que espera um valor de tipo LogT τ (com modo log), o objecto original terá de ser convertido para ficar com o modo esperado pelo método. O modo log inclui um método de coerção
com assinatura:
#log_up: ∀T≤ * INTERFACE({'==':SAMET→Bool}).T→(LogT T)
cuja simples existência determina que aquela conversão se pode realizar para todos os tipos τ
tais que τ≤ * INTERFACE({'==':SAMET→Bool})). Adicionalmente, o corpo desse método descreve
como a conversão se efectua. Este método de coerção é, portanto, um elemento da faceta
estática do modo log.
O segundo exemplo é um pouco mais complicado. Se um objecto com tipo base τ e modo
composto const value log for passado para um parâmetro com o mesmo tipo base e modo composto lazy log, então existirá uma coerção composta a realizar e uma cadeia de procedimentos
de conversão a aplicar. Neste caso, o modo do argumento terá de ser primeiro despromovido a
log e depois promovido a lazy log para, finalmente, ficar a par com o modo requerido pelo parâmetro.
Um outro aspecto estático tem a ver com a indicação de que o modo log deve suportar uma
inicialização implícita das variáveis lógicas. Para isso basta definir um construtor público de
nome #zero, já que a definição da operação de inicialização implícita, #$def_init, depende da
existência do método #zero e usa-o quando ele existe. Incidentalmente, o construtor #zero do
158
OM – Uma linguagem de programação multiparadigma
modo log gera objectos lógicos não ligados, ou seja, variáveis livres no sentido da linguagem
Prolog.
Um último aspecto estático: o modo log não suporta a operação de atribuição, impedindo
assim as variáveis lógicas de serem alvo dessa operação. Este efeito consegue-se, não definindo deliberadamente o método #$def_assign no modo log.
9.1.3 O que é um modo?
Nesta secção vamos considerar apenas a faceta dinâmica dos modos.
Um modo é um construtor de classes especial (cf. secção 6.1.1), que permite criar classes
não-paramétricas através uma nova operação de instanciação (formalizada na secção 9.2.2.2).
Cada modo gera também um tipo-objecto paramétrico especial, que será designado por operador de modo (formalizado na secção 9.2.3).
Um modo, tem a seguinte forma sintáctica:
m =ˆ mode X≤* INTERFACE(ϒ B).RM
Trata-se duma entidade paramétrica com um único parâmetro X, caracterizada por uma interface-limite INTERFACE(ϒB), imposta ao tipo-parâmetro X, e por um registo de componentes públicas RM, designadas por componentes específicas do modo. Tomando o exemplo do modo
log, este modo caracteriza-se pela interface-limite INTERFACE({'==':SAMET→Bool}) e por diversas componentes específicas definidas no seu corpo, entre as quais se encontram os métodos
isVar e '==' (cf. secção 9.1.2.1).
A operação de instanciação dum modo m com um tipo-objecto τ denota-se por (m τ). Quando um modo m é instanciado com um tipo-objecto τ compatível com a sua interface-limite, o
que se obtém é uma classe não-paramétrica. Neste sentido, um modo é efectivamente um construtor de classes. Por sua vez, cada instância (m τ) do modo m gera um tipo-objecto MT τ que
depende de τ. Isto mostra que um modo gera um tipo-objecto paramétrico especial, neste caso
denotado por MT, que designaremos por operador de tipo de modo, ou, mais simplesmente,
por operador de modo. Por exemplo: dado um tipo-objecto τ compatível com a interface-limite
do modo lógico, a expressão log τ representa uma classe não-paramétrica geradora de objectos
lógicos do tipo LogT τ.
Os objectos gerados pela classe (m τ) – ditos objectos com modo m – têm uma funcionalidade enriquecida relativamente aos objectos do tipo τ. Os objectos da classe (m τ) suportam todas
as componentes do tipo τ e, adicionalmente, as componentes específicas do modo m. Em caso
de conflito (ou seja, de sobreposição) as componentes específicas do modo m têm prioridade
sobre as componentes do tipo τ.
Um modo m é, em muitos aspectos, semelhante a uma classe paramétrica com um único parâmetro. De facto, da instanciação duma classe paramétrica P com um tipo-objecto τ compa-
9 Modos
159
tível também resulta uma classe não-paramétrica, neste caso P[τ] . Além disso as classes paramétricas geram tipos-objecto paramétricos (cf. 6.1.1) tal como os modos. A diferença entre a
classe P[τ] e a classe (m τ) é a seguinte: enquanto que os objectos da classe P[τ] são constituídos
pelas componentes específicas da classe P apenas, os objectos da classe (m τ) contêm todas as
componentes do tipo τ mais as componentes específicas do modo m.
9.1.4 Implementação dum modo
A implementação dum qualquer modo m
m =ˆ mode X≤* INTERFACE(ϒ B).RM
descreve a funcionalidade privada e pública dos objectos de tipo MT X, gerados pela classe
(m X). Sobre X apenas se sabe que verifica a restrição de instanciação do modo m. Há três regras que toda a implementação do modo m tem de cumprir com rigor:
• A implementação de m tem de garantir que todo o objecto z:MT X, gerado pela classe
(m X), fica internamente ligado a um objecto obj de tipo X . Nesta circunstância o objecto
obj designa-se por objecto conexo de z. O seguinte diagrama exprime a relação entre o
objecto com modo z e o seu objecto conexo:
z:MT X
obj:X
• A implementação de m tem de suportar um método de instância chamado $access que
coloque à disposição do objecto z (e das equações semânticas da linguagem) uma forma universal de acesso ao objecto conexo. Faz sentido requerer o método $access , pois
os detalhes do estabelecimento da ligação interna entre z e o seu objecto conexo obj variam de modo para modo. Actualizamos o diagrama anterior, agora considerando a
existência do método $access :
z:MT X
$access()
ob:X
obj:X
• Estabelecida a ligação entre z e o seu objecto conexo obj, e definido o método $access, o
objectivo da implementação de m é que z seja visto como uma versão enriquecida ou
modificada de obj. Não é difícil obter este efeito: todos os acessos a z que envolvam as
componentes do tipo τ são reencaminhados para obj usando o método $access (a equação
semântica do modo trata desta questão automaticamente); todos os acessos a z que envolvam as componentes específicas do modo m são tratados ao nível do próprio objecto
z.
Este esquema de implementação é perfeitamente satisfatório, tanto no caso dos modos simples como no caso dos modos mais complicados. É apenas preciso considerar uma circunstância suplementar que surge frequentemente durante a implementação dos modos mais sofistica-
160
OM – Uma linguagem de programação multiparadigma
dos: a necessidade de, transitoriamente, representar o objecto conexo sob uma forma descritiva
indirecta. É o que se acontece no modo log que usa restrições lógicas como descrições indirectas e temporárias dos objectos conexos que são soluções dessas restrições. Nestes modos
mais complicados, o método $access tem de se preocupar com a situação em que o objecto conexo se apresenta sob uma forma descritiva indirecta. Nessa situação, $access é obrigado a procurar uma forma explícita para o objecto conexo e, caso não a consiga encontrar, a computação tem de ser abortada (ou, pelo menos, gerada uma excepção, com se faz na linguagem OM
final).
Para exemplificar, vamos apresentar uma implementação completa dum modo. Consideramos o modo mais simples que faz o mínimo de sentido – o modo neutral:
neutral
=ˆ mode X≤* INTERFACE({}).{
priv_obj=ref (nil:X),
$access=λz:Unit. (deref self.priv_obj),
#new=λa:X.(let p=SELFC#priv_new () in (p.priv_obj:=a; p)) }
Este modo introduz uma variável de instância privada, priv_obj, que se destina a guardar directamente o objecto conexo (note que os objectos com modo neutral são simples contentores).
Define também o método de instância $access, o qual, neste caso, se limita a produzir o valor da
variável priv_obj. Define ainda um construtor público chamado #new, sem o qual seria impossível criar objectos com modo neutral no exterior da definição.
9.2 Semântica de L9
Formalizamos agora a semântica da linguagem L9. A tabela das equações semânticas de L9
encontra-se no início do presente capítulo.
Em L9 continuamos a tratar a questão do estado mutável da mesma forma que em L8 (cf.
8.1). Ou seja, nas discussões informais e nos exemplos assumimos que L9 suporta objectos
mutáveis, mas nas equações semânticas ignoramos esta faceta da linguagem. Não vale a pena
complicar, pois a formalização da linguagem L7& já mostra como essa questão pode ser tratada (cf. secção 7.3).
9.2.1 Semântica dos tipos
Um tipo novo que surge em L9 é o tipo dos modos, ou tipo-modo, com a forma:
MODETYPE X≤ * INTERFACE(ϒ B).ϒM
onde ϒM≤{$access :Unit→X}
Neste novo tipo, INTERFACE(ϒ B) é a interface-limite imposta ao tipo-parâmetro X e ϒM é o
tipo-registo das componentes específicas do modo. Este último tipo-registo tem obrigatoriamente de incluir uma componente $access com o tipo Unit→X.
A codificação deste tipo em F+ é a seguinte:
9 Modos
161
∀ X≤* INTERFACE(ϒ B).
CLASSTYPE(pub(ϒB)[X/SAMET]⊕ϒM)
Ela reflecte o conhecimento parcial que existe sobre os tipos X que podem ser usados para instanciar o modo. No corpo deste tipo paramétrico encontra-se um tipo-gerador de meta-objectos
que contém: (1) as componentes públicas de X que podem ser previstas considerando a interface-limite INTERFACE(ϒ B), ou seja pub(ϒB)[X] (repare que X≤pub(ϒB)[X]); (2) as componentes
específicas do modo, ou seja, ϒM. Note que as componentes específicas têm precedência sobre
as componentes com origem em X.
Se o tipo dos modos fosse interpretado como o tipo duma classe paramétrica normal então
o seu tipo seria mais simplesmente:
∀ X≤* INTERFACE(ϒ B).
CLASSTYPE(ϒ M)
o que confirma que um modo difere duma classe paramétrica apenas na forma como as componentes com origem em X são tratadas.
Adiamos para a secção 9.2.3 a introdução dos novos tipos de ordem superior, operadores
de modo, porque só nessa secção estará criado o contexto necessário à sua apresentação.
9.2.2 Semântica dos termos
Em L9 existem apenas duas novas formas de termos: os modos e as instanciações de modo.
9.2.2.1 Modos
Um modo tem a forma
mode X≤* INTERFACE(ϒ B).RM
:MODETYPE X≤* INTERFACE(ϒ B).ϒM
onde INTERFACE(ϒB) é a interface-limite a que está submetido o parâmetro X , e é RM o registo
das componentes específicas do modo. Definimos a semântica deste novo termo através da seguinte tradução para a seguinte classe paramétrica de L8:
λX≤* INTERFACE(ϒ B).
class{access[pub(ϒB)[X/SAMET]] + R M}
Esta classe contém: (1) as componentes públicas do tipo-parâmetro X que podem ser previstas
considerando a interface-limite INTERFACE(ϒB); (2) as componentes específicas do modo, incluindo o método de instância $access.
As componentes com origem no tipo-parâmetro X têm de ser introduzidas duma forma especial, visto o seu conteúdo semântico nunca ser conhecido ao nível do modo; essa informação
só estará disponível em tempo de execução nos objectos conexos dos objectos com modo. Para
aceder a essa informação em tempo de execução, usamos uma técnica simples: por cada componente l que ocorre em INTERFACE(ϒB), adicionamos à classe que resultará da instanciação do
162
OM – Uma linguagem de programação multiparadigma
modo um método público de reencaminhamento da forma l=λz:Unit.(self.$access ()).l. Recordamos que o método $access define uma forma de acesso ao objecto conexo, disponível em todos
os objectos com modo. (O método de reencaminhamento tem um parâmetro artificial z:Unit,
que temos necessidade de introduzir por L9 usar a estratégia de avaliação call-by-value.).
Dentro dum modo, o programador tem acesso a todas as componentes específicas do modo,
privadas e públicas, mais as componentes indicadas na sua interface-limite: o tipo que disponibiliza esta funcionalidade é SELFT.
9.2.2.2 Instanciação dum modo
Tratemos agora do caso da instanciação dum modo com um tipo-objecto. Considere o seguinte
modo genérico m:
m
=ˆ mode X≤* INTERFACE(ϒ B).RM
: MODETYPE X≤ * INTERFACE(ϒ B).ϒM
A instanciação deste modo m com um tipo T≤* INTERFACE(ϒ B) escreve-se da seguinte forma
simples:
mT
Com o objectivo de estudar a semântica da instanciação dum modo, vamos ter de considerar
duas possibilidades, bem distintas, relativamente a T:
• T é um tipo concreto: portanto T≡OBJTYPE(ϒ) com OBJTYPE(ϒ)≤* INTERFACE(ϒ B);
• T é uma variável de tipo: neste caso, previamente introduzida numa outra entidade
paramétrica sob a restrição T≤ * INTERFACE(ϒ), sendo INTERFACE(ϒ) ext INTERFACE(ϒB).
Conseguiremos tratar estes dois casos de forma uniforme, mediante a introdução do seguinte tipo-registo ϒT:
ϒT
=ˆ pub(ϒ)[T/SAMET]
ϒT inclui
todas as componentes públicas estaticamente conhecidas de T. Vamos provar que
ϒT é um supertipo de T:
Teorema 9.2.2.2-1 Nas condições anteriores, verifica-se a asserção:
T≤ϒT
Prova: Se T for um tipo concreto então temos:
T=OBJTYPE(ϒ)
⇒ T=pub(ϒ)[T/SAMET]
⇒ T=ϒT
⇒ T≤ϒT
Se T for uma variável, então temos:
por definição de OBJTYPE(ϒ)
por definição de ϒ T
por [Sub =]
9 Modos
163
T≤ * INTERFACE(ϒ)
⇒ T≤INTERFACE(ϒ)[T]
⇒ T≤pub(ϒ)[T/SAMET]
⇒ T≤ϒT
por definição de ≤ *
por definição de INTERFACE(ϒ)
por definição de ϒ T
O tipo que se atribui ao termo (m T) é um tipo-classe:
mT
: CLASSTYPE(ϒT⊕ϒM[T/X])
Isso significa que pretendemos que o termo (m T) seja visto como uma classe não-paramétrica
que inclui todas as componentes de T – ou pelo menos as que se podem prever estaticamente,
no caso de T ser uma variável – mais as componentes específicas do modo m. É suficiente inspeccionar o tipo-classe CLASSTYPE(ϒT⊕ϒM[T/X]) para se concluir que: (1) se T for um tipo-objecto concreto, como T=ϒT, então todas as componentes de T estão presentes em
CLASSTYPE(ϒ T⊕ϒM[T/X]); (2) se T for uma variável de tipo tal que T≤ * INTERFACE(ϒ), como
T≤ϒT, então todas as componentes que se podem prever estão presentes no tipo-classe
CLASSTYPE(ϒ T⊕ϒM[T/X]).
Neste momento estamos em condições de estudar a equação semântica que descreve como
se processa a instanciação dum modo m com um tipo T, para se obter a classe não-paramétrica
(m T). A equação encontra-se na tabela que se encontra no início do presente capítulo. Na equação, começamos por determinar:
S=m[T]
usando o modo m como se duma classe paramétrica normal se tratasse (repare que [.] representa a operação de instanciação de entidades paramétricas normais).
Note que a classe S=m[T] é já uma boa aproximação da classe pretendida, (m T): efectivamente, S já inclui todas as componentes específicas do modo, incluindo o método de instância
$access , mais todos os métodos de reencaminhamento referentes às componentes que ocorrem
na interface-limite do modo.
Falta só o detalhe de adicionar à classe S os métodos de reencaminhamento referentes às
componentes que estão em ϒ T mas não em ϒB. Ora os métodos de reencaminhamento são métodos de instância públicos, pelo que precisamos de aceder ao gerador interno de instâncias de
S , #priv_gen, para com base nele criarmos um gerador interno enriquecido, e uma nova classe
com este novo gerador no seu interior. Para fazermos tudo isto, seguimos o padrão geral da codificação de classes em L8. Começamos por introduzir novos nomes SAMET, SELFC, hide ,
SELFC e adaptamos o gerador polimórfico S ao contexto destes novos nomes, aplicando S a
todos eles:
_SUPERC = S[SAMET][SELFT] hide SELFC
Depois extraímos o gerador de instâncias _SUPERC.#priv_gen e usamo-lo na criação dum novo
gerador de instâncias:
164
OM – Uma linguagem de programação multiparadigma
#priv_gen = λself:SELFT.(access[ϒT] + _SUPERC.#priv_gen self)
Finalmente, este novo gerador é usado para substituir aquele que se encontra em _SUPERC, terminando assim a criação da nova classe. Note que o novo gerador inclui, como pretendíamos,
um método de reencaminhamento por cada componente de ϒT: a primitiva access definida na
tabela de equações trata dessa questão.
Terminada que foi a criação da nova classe (m T), verifica-se que apenas as componentes
estaticamente conhecidas de T, i.e. as que estão presentes em ϒT, ficam com um método de ligação na classe gerada. Isso não é problema pois as componentes estaticamente desconhecidas
de T estão, em todo o caso, inacessíveis devido à natureza estática do nosso sistema de tipos.
Note ainda que o tipo T ocorre diversas vezes no interior da equação semântica da classe
(m T), sendo portanto errónea a ideia de que T seria substituído pelo seu supertipo ϒT ao longo
de toda essa equação semântica. Aliás, se tal fosse feito a equação ficaria mal tipificada. Na
realidade T é substituído por ϒT apenas nos contextos que reflectem o facto de só componentes
estaticamente conhecidas de T terem um método de ligação na classe (m T).
9.2.2.3 Boa tipificação da equação semântica
Agora, importa ver se estão bem tipificadas as expressões M[T] e (S[SAMET][SELFT] hide SELFC)
que ocorrem na equação semântica do termo (m T).
está bem tipificada, pois as restrições que o parâmetro T tem de verificar (indicadas no
final da equação) garantem que se tem sempre T≤ * INTERFACE(ϒ B). Este facto é verdadeiro,
independentemente de T ser um tipo concreto ou uma variável de tipo (no caso da variável de
tipo é preciso usar a definição de ext para tirar esta conclusão).
M[T]
Relativamente a (S[SAMET][SELFT] hide SELFC) é preciso verificar se as três seguintes condições são válidas:
SAMET≤* INTERFACE(ϒ T⊕ϒM[T/X]) ⇒
SAMET≤* INTERFACE(pub(ϒB)[T/SAMET]⊕ϒM[T/X])
SAMET≤* INTERFACE(ϒ T⊕ϒM[T/X]) ⇒
SELFT≤IINTERFACE(ϒT⊕ϒM[T/X])[SAMET] ⇒
SELFT≤IINTERFACE(pub(ϒB)[T/SAMET]⊕ϒM[T/X])[SAMET]
SAMET≤* INTERFACE(ϒ T⊕ϒM[T/X]) & SELFT≤IINTERFACE(ϒ T⊕ϒM[T/X])[SAMET] ⇒
#IINTERFACE(ϒT⊕ϒM[T/X])[SAMET][SELFT] ≤
#IINTERFACE(pub(ϒB)[T/SAMET]⊕ϒM[T/X])[SAMET][SELFT]
Felizmente que a validade das condições anteriores pode ser deduzida da validade da asserção simples ϒ T≤pub(ϒB)[T/SAMET] pois, como é fácil de ver, esta asserção implica as seguintes
três condições que são, todas elas, mais fortes que as anteriores:
9 Modos
165
INTERFACE(ϒ T⊕ϒM[T/X]) ≤ INTERFACE(pub(ϒB)[T/SAMET]⊕ϒM[T/X])
IINTERFACE(ϒT⊕ϒM[T/X]) ≤ IINTERFACE(pub(ϒB)[T/SAMET]⊕ϒM[T/X])
#IINTERFACE(ϒT⊕ϒM[T/X]) ≤ #IINTERFACE(pub(ϒB)[T/SAMET]⊕ϒM[T/X])
Assim, só temos de provar a asserção ϒT≤pub(ϒB)[T/SAMET] para garantir a boa tipificação
do termo (S[SAMET][SELFT] hide SELFC).
Teorema 9.2.2.3-1 Sob as restrições a que o nome T está submetido na equação semântica
do termo (m T), verifica-se a propriedade:
ϒT≤pub(ϒB)[T/SAMET]
Prova: Consideremos primeiro o caso em que T≡OBJTYPE(ϒ)≤* INTERFACE(ϒ B). Neste situação,
já vimos na prova do teorema 9.2.2.2-1 que T=ϒT. Assim ϒT≤* INTERFACE(ϒ B), ou seja, usando
a definição de interface, ϒT≤pub(ϒB)[T/SAMET], como pretendíamos.
Consideremos agora o caso em que T é uma variável de tipo tal que T≤* INTERFACE(ϒ) com
INTERFACE(ϒ) ext INTERFACE(ϒB). Para começar, segundo o lema 5.3-6 verifica-se a condição
INTERFACE(ϒ)[T]≤INTERFACE(ϒ B)[T], que se reescreve pub(ϒ)[T/SAMET]≤pub(ϒB)[T/SAMETB].
Mas sendo assim, da definição de ϒ T=pub(ϒ)[T/SAMET] sai imediatamente ϒT≤pub(ϒB)[T/SAMET],
como pretendíamos.
9.2.3 Operadores de modo
Na secção 9.1.3, verificámos que um modo gera uma nova forma de tipo de ordem superior
que designámos por operador de modo. Um operador de modo tem a seguinte forma sintáctica:
MODEOP X.ϒ M
A codificação em F+ dum operador de modo é efectuada como se dum tipo-objecto paramétrico normal, gerado por uma classe paramétrica, se tratasse. Assim, a codificação do tipo anterior é a seguinte:
ΛX.OBJTYPE(ϒM)
No entanto, a operação de instanciação dum operador de modo tem particularidades especiais relativamente é operação de instanciação dum tipo-objecto paramétrico normal. A definição da nova operação de instanciação é a seguinte:
Seja ϕ um operador de modo e seja T um tipo concreto T≡OBJTYPE(ϒ) ou uma variável de
tipo T≤* INTERFACE(ϒ) (cf. secção 9.2.2.2). Seja ϒT =ˆ pub(ϒ)[T/SAMET] o tipo que introduzimos
na secção 9.2.2.2. Então a operação de instanciação (ϕ T), de ϕ com T, define-se então seguinte
forma:
ϕT
=ˆ ϒT⊕ϕ[T]
166
OM – Uma linguagem de programação multiparadigma
Esta é a definição que incluímos na tabela de equações semânticas do início do presente capítulo. Note que, na expressão da direita, ϕ[T] é sempre um tipo-objecto, ou seja um tipo-registo
definido recursivamente sobre uma variável de tipo SAMET. Quanto a ϒ T, trata-se dum tipo-objecto não recursivo, no qual SAMET não tem qualquer ocorrência.
9.3 Discussão sobre L9
Nesta secção, essencialmente, discutimos aspectos adicionais sobre o mecanismo dos modos
que não tiveram cabimento nas subsecções anteriores.
A decisão de separar os conceitos de classe e de tipo-objecto remonta à linguagem L4 (cf.
secção 4.1.3). A partir deste capítulo, os modos serão uma excepção a esta regra. Repare na seguinte ideia: ao declararmos uma variável com o tipo LogT Nat, pretendemos que esta variável
assimile características semânticas, as das variáveis lógicas definidas no modo log, e não apenas propriedades derivadas de informação de tipo. Ora tal só se pode garantir se identificarmos
o modo log com o operador de modo LogT. Para obter esse efeito recorremos à técnica da
secção 4.3.6: introduzimos na definição do modo log um identificador discriminante único –
$object_with_mode_log (ver a definição do modo log na biblioteca padrão da linguagem OM – cf.
secção 12.5). Relativamente aos outros modos procedemos de igual forma.
Nas equações semânticas da linguagem L9, não previmos a possibilidade dum modo herdar
componentes duma classe. Este caso não foi tratado apenas por uma questão de simplificação
da apresentação do mecanismo dos modos: imitando a equação da subclasse de L8, não é difícil generalizar a equação do modo para permitir herança de componentes.
Dado um modo m e um tipo-objecto τ qualquer, a expressão m τ e representa uma classe.
Assim, em princípio, parece fazer sentido a possibilidade de herdar a partir de m τ , ou seja
declarar m τ como superclasse duma outra classe C. No entanto, acontece que, neste caso,
surge uma situação paradoxal: os objectos da subclasse C herdam a funcionalidade do modo m ,
mas, no entanto, tecnicamente, são objectos sem modo pois C é uma classe normal, directamente definida. Assim, tanto a linguagem L9 como a linguagem final OM proíbem herança a
partir de classes que resultem da instanciação de modos.
9.4 Conclusões
No início dos anos 80, começaram a ser estudados e desenvolvidos sistemas de programação
reflexivos, sistemas geralmente de base dinâmica e suportando introspecção e a modificação
dos mecanismos básicos em tempo de execução [Smi83, KRB91, DG87].
A linguagem L9 é também, em certo sentido uma linguagem reflexiva, na medida em que,
usando a própria linguagem, o programador pode escrever uma nova colecção de modos que
estendem L9 semanticamente, inclusivamente modificando alguns dos mecanismos básicos da
9 Modos
167
linguagem. Contudo, estamos perante uma forma de reflexão estática já que a linguagem alterada fica com uma semântica que não pode ser modificada em tempo de execução.
Repare também que num certo sentido, a própria noção de classe também suporta um certo
grau de reflexão: por exemplo em C++, um programador pode escrever um programa em C++
(uma colecção de classes) que, num certo sentido, altera as características do C++: no sentido
em que a linguagem fica funcionalmente mais rica e com mais tipos de dados.
Estas comparações têm interesse pois ajudam a situar a noção de modo no contexto dum
contínuo de reflexão que se inicia no mecanismo das classes e se estende, passando pelos modos, até aos sistemas reflexivos dinâmicos que têm os interpretadores metacirculares como limiar superior.
O sucesso das linguagens orientadas pelos objectos explica-se, em parte, pelas características de extensibilidade limitada que o mecanismo das classes introduz na linguagem. O mecanismo dos modos encontra-se um degrau acima do mecanismo das classes, mas ainda dentro
de território estático.
Capítulo 10
Sistema de coerções
Na secção 10.1, motivamos a necessidade de suportar um sistema de coerções extensível na
linguagem L10 e determinamos alguns requisitos preliminares desse sistema que serão levados
em consideração nas secções subsequentes. Na secção 10.2, introduzimos e estudamos uma
versão preliminar do sistema de coerções chamada sistema natural. Na secção 10.3, introduzimos e estudamos a versão final do sistema de coerções designada por sistema prático.
10.1 Conceitos e mecanismos de L10
A faceta estática do mecanismo dos modos pressupõe a existência dum sistema de coerções
extensível, suportado a nível primitivo. É esse mecanismo que introduzimos no presente capítulo como parte integrante da linguagem L10.
As coerções têm um papel importante na linguagem L10, especialmente as coerções de
modo (cf. secção10.1.3). Estas tornam a linguagem mais usável na prática, ao permitirem que
expressões cujos tipos difiram apenas no modo possam ser usadas de forma intermutável. Para
dar um exemplo, são as coerções de modo que permitem que uma expressão de tipo Int possa
ser usada directamente como argumento duma função com um parâmetro de tipo ConsT Int.
Seria possível dispensar as coerções de modo, mas nesse caso à custa do recurso continuado à
utilização de funções de conversão usadas de forma explícita.
O sistema de coerções de L10 também introduz uma forma muito básica de call-by-name
que, no contexto das classes primitivas da linguagem OM, é explorada na definição de diversas primitivas dessa linguagem: comandos da linguagem, métodos '&&' e '||' da classe Bool , etc.
(cf. secção 10.2.1).
O sistema de coerções de L10 incorpora um pequeno número de regras fixas, ditas regras
básicas, e admite ser estendido com novas regras, ditas regras extra, definidas usando os denominados métodos de coerção (cf. secção 10.2.1). O sistema de coerções de L10 não inclui
qualquer regra básica que especifique uma coerção de modo: todas as regras que definem coerções de modo são para ser introduzidas dentro dos modos usando métodos de coerção. Isso faz
todo o sentido: quem estende a linguagem com um novo modo é que tem a possibilidade de
determinar quais são as coerções que esse novo modo deve suportar, e qual a implementação
das respectivas funções de conversão.
170
OM – Uma linguagem de programação multiparadigma
Em resultado duma decisão de desenho da linguagem, só se permite a introdução de regras
extra no nível privilegiado da linguagem (cf. secção 11.4), ou seja dentro dos modos e dentro
das classes primitivas. Para o utilizador da linguagem que não a pretenda estender, o sistema
de coerções da linguagem deve ser encarado como um sistema predefinido rígido.
Um exemplo de linguagem prática que, tal como a linguagem L10, também possui um
sistema de coerções extensível, é a linguagem C++. Contudo o C++ só suporta coerções de
tipos atómicos, o que torna triviais todos os problemas envolvidos no seu sistema de coerções.
Já na nossa linguagem L10, existe a necessidade de suportar coerções de tipos estruturados.
10.1.1 Coerções e relação de coerção
Uma coerção é uma conversão de tipo implícita que é decidida e inserida num programa em
tempo de compilação, sem qualquer intervenção do programador. Quando uma expressão
exp do tipo υ ocorre num contexto que requer um tipo τ distinto, o sistema de coerções intervém para determinar se a conversão implícita de tipo υ-->τ é suportada pela linguagem. Em
caso afirmativo, a ocorrência da expressão exp é substituída pela nova expressão (f exp), onde
f: υ→τ representa a função de conversão que implementa a coerção. Se aquela conversão implícita de tipo não for suportada, a ocorrência de exp é considerada um erro.
Em cada contexto Γ, o conjunto das coerções suportadas por uma linguagem de programação é um conjunto de pares ordenados de tipos, ou seja uma relação binária sobre tipos.
Designaremos essa relação por relação de coerção e usaremos o símbolo ≤ c para a representar.
Uma linguagem de programação não deve ser muito generosa quanto à variedade de coerções suportadas. Caso contrário existe o perigo de verdadeiros erros de tipo passarem desapercebidos por serem automaticamente corrigidos pelo sistema de coerções. Quanto às coerções
efectivamente suportadas, estas devem ser conservadoras (widening), garantindo que o
conteúdo informativo dos valores convertidos não se perde [Seb93]: o contrário seria perigoso
pois as coerções actuam implicitamente. Por exemplo, a coerção natural de valores inteiros em
valores reais é conservadora, mas já a coerção inversa não o é. Na linguagem L10, as regras
básicas do sistema de coerções, assim como as regras extra da biblioteca padrão (listadas na
secção 10.2.3), suportam apenas coerções conservadoras.
10.1.2 Sistema de coerções
Um sistema de coerções é um sistema de prova sobre juízos de coerção da forma:
Γ υ≤cτ
o tipo υ é implicitamente convertível para o tipo τ no contexto Γ
Cada regra dum sistema de coerções têm uma função de conversão associada, sendo o seguinte o formato geral de introdução dum par <regra de coerção, função de conversão associada>:
10 Sistema de coerções
[Coerção …]
…
Γ υ≤cτ
171
(Γ υ≤cτ)
=ˆ λf:υ. …
As regras do sistema de coerções permitem deduzir os juízos de coerção válidos do sistema.
Quanto às funções de conversão associadas, elas servem para construir, através de composição, as funções de conversão irão implementar os juízos de coerção válidos (cf. secção 10.2.5).
A parte dum juízo de coerção que sucede o símbolo chama-se asserção de coerção e tem
a forma geral υ≤cτ. Todas as variáveis livres que ocorrem numa asserção de coerção têm de estar declaradas no contexto respectivo.
Em situações em que o contexto Γ permanece invariante, é preferível trabalhar com simples
asserções de coerção, deixando o contexto implícito, em vez de trabalhar com juízos de coerção completos. Esta é uma prática comum que seguiremos em muitas ocasiões. Em conformidade, definimos asserção de coerção válida como sendo um juízo de coerção válido no qual o
contexto tenha sido deixado implícito.
Mais adiante, formalizaremos a relação de coerção de L10 por meio dum sistema de coerções. Estudaremos duas versões deste sistema: uma versão preliminar, designada por sistema
natural (cf. secção 10.2), e uma versão final, designada por sistema prático (cf. secção 10.3).
10.1.3 Coerções de modo
Uma coerção de modo é uma conversão de tipo implícita na qual os dois tipos intervenientes
diferem apenas nos seus modos. Para exemplificar, consideremos as asserções de coerção
υ≤cGenT υ e ValueT υ≤ cLazyT LogT υ, ambas suportadas pela linguagem OM: a primeira asserção
permite enriquecer com gen o modo dum qualquer tipo υ; a segunda permite converter o modo
value em lazy log.
As coerções de modo têm um papel importante na linguagem OM. Elas permitem que expressões cujos tipos difiram apenas no seu modo possam ser usadas de forma intermutável, o
que facilita o uso da linguagem. De forma limitada, as coerções de modo também podem ser
usadas para introduzir um pouco açúcar sintáctico, o que permite polir um pouco mais a linguagem.
Vamos apresentar um exemplo simples que envolve coerções de modo e que ilustra os dois
aspectos referidos.
O modo gen (modo dos geradores) suporta um operador de disjunção ‘|’ aplicável a um par
de geradores: em resultado da sua aplicação é produzido um novo gerador, mais complexo,
que junta os valores do primeiro gerador com os valores do segundo gerador. A assinatura do
operador ‘|’ é a seguinte:
'|':∀ X≤ * {}. GenT X→GenT X→GenT X
Consideremos agora a expressão:
172
OM – Uma linguagem de programação multiparadigma
1|2
Nesta expressão, o operador '|' é aplicado a dois números naturais sem modo. Contudo, o operador espera argumentos de tipo GenT X , ou seja com modo gen. Portanto, à partida, a expressão 1|2 é um erro de tipo.
O que muda a situação é o facto do modo gen incluir um método de coerção, concretamente
#gen_up: X→GenT X, o qual enriquece o sistema de coerções com a regra extra, X≤extraGenT X,
donde se deduz a asserção Nat≤cGenT Nat. Assim, no final, a expressão 1|2 acaba por ser automaticamente convertida na expressão:
(#gen_up 1) | (#gen_up 2)
a qual já está bem tipificada.
Com a interpretação atribuída à expressão 1 | 2, esta, e outras expressões da mesma forma,
passam a poder ser consideradas como expressões literais sobre o tipo GenT Nat.
10.1.4 Funções de conversão sem redundância
Mais adiante, na secção 10.3.4, estudaremos dois procedimentos de prova no contexto do sistema de coerções de L10: o procedimento de prova normalizado e o procedimento de prova
prático. O segundo procedimento será adoptado como procedimento de prova oficial do sistema de coerções de L10.
O procedimento de prova prático tem a particularidade de, perante um juízo Γ υ≤cτ que
possa ser provado de diversas formas, construir para esse juízo uma árvore de prova com tamanho mínimo (cf. definição 10.2.5-4). Assim, o procedimento de prova prático associa a cada
juízo de coerção válido uma função de conversão que minimiza o número de passos de conversão elementares.
Do ponto de vista da elegância formal, da eficiência e mesmo da intuição 2 , a minimização
do número de passos elementares nas funções de conversão faz todo o sentido. Uma quarta razão, bem mais específica, que nos levou a considerar esse aspecto decorre da conjugação dos
dois seguintes factores:
• Os modos mais sofisticados representam o objecto conexo por meio duma forma descritiva indirecta (cf. secção 9.1.4);
• Ao serem aplicadas, certas coerções de despromoção de modo, e.g. LazyT Nat≤ cNat, forçam a explicitação do objecto conexo. Esta explicitação pode ser um problema se for
efectuada prematuramente, ao arrepio da lógica de implementação do modo (pode mesmo fazer abortar a computação, em certos modos).
2 Quando se tenta compor, mentalmente, uma função de conversão de um tipo T para um tipo T , a tendência natural é para
1
2
imaginar uma função simples, e não uma função desnecessariamente complicada.
10 Sistema de coerções
173
Se permitíssemos que as funções de conversão incluíssem passos de conversão intermédios redundantes, introduzidos de forma injustificável e semialeatória, então haveria o risco de se forçar prematura e inesperadamente a explicitação de algum objecto conexo.
Vamos ilustrar a situação usando um exemplo simples, no qual intervém o modo lazy, um
modo que representa o objecto conexo por meio duma forma descritiva indirecta.
Consideremos o tipo ConstT LazyT Nat, com modo composto const lazy, e o tipo ValueT
LazyT Nat, com modo composto value lazy. Os métodos de coerção existentes nos modos de biblioteca da linguagem OM (cf. capítulo 12), conjugados com as regras básicas do sistema de
coerções (cf. 10.2.2), permitem provar a seguinte asserção de infinitas formas diferentes (cf.
secção 10.2 .7.1):
ConstT LazyT Nat≤cValueT LazyT Nat
Às várias formas de provar esta asserção, estão associadas funções de conversão com organizações distintas (arbóreas ou lineares) e constituídas por um número variável de passos de
conversão intermédios. Vejamos dois caminhos de conversão lineares que a nossa asserção de
coerção admite.
O primeiro caminho, produzido pelo procedimento de prova normalizado, tem 4 passos de
conversão elementares (cf. demonstração do teorema 10.3.5-2):
ConstT LazyT Nat —> LazyT Nat —> Nat —> LazyT Nat —> ValueT LazyT Nat
Este caminho começa por eliminar gradualmente o modo composto de partida, const lazy, para
depois instalar, a partir do nada, um novo modo composto, value lazy. Os dois passos elementares do meio são redundantes pois não provocam alteração de modo. No entanto, o primeiro deles, LazyT Nat—>Nat, força, por efeito lateral, a explicitação do objecto conexo. Ora isso prejudica gravemente a intenção que preside ao modo lazy: a intenção de suportar um mecanismo de
avaliação preguiçosa (cf. secção 12.4).
O segundo caminho, produzido pelo procedimento de prova prático, é constituído por 2
passos de conversão elementares (cf. demonstração do teorema 10.3.5-2):
ConstT LazyT Nat —> LazyT Nat —> ValueT LazyT Nat
Este caminho preserva nos objectos convertidos um núcleo fixo, de tipo LazyT Nat, que não é
sujeito a qualquer conversão. Neste caso, a forma descritiva indirecta dos objectos conexos do
modo lazy é preservada.
Este exemplo, que acabámos de apresentar, é paradigmático relativamente ao que se pode
esperar dos dois procedimentos de prova considerados:
• O procedimento de prova normalizado tende a inserir de forma injustificável passos intermédios desnecessários nas funções de conversão. Esses passos podem forçar desnecessariamente, e por vezes prematuramente, a explicitação do objecto conexo.
174
OM – Uma linguagem de programação multiparadigma
• O procedimento de prova prático nunca gera passos de conversão intermédios redundantes (por construção). Uma função gerada pelo procedimento de prova prático, se
forçar a explicitação do objecto conexo, nunca o fará pelas razões espúrias do procedimento de prova normalizado.
Está pois explicada a quarta razão que nos levou a optar pelo procedimento de prova prático: o facto deste procedimento só gerar funções sem redundância, afinal uma medida básica de
sanidade.
10.2 O sistema natural
O sistema natural define de forma simples e intuitiva, ou seja de forma natural, uma relação
binária extensível de coerção entre tipos. Ele é deliberadamente introduzido sem grandes preocupações de natureza técnica, o que faz com que padeça das seguintes deficiências: indecidibilidade, indeterminismo e ambiguidade. Estes problemas serão resolvidos na secção 10.3, onde
será introduzido o sistema pratico, uma versão mais evoluída, derivada do sistema natural.
O sistema de coerções de L10 inclui um pequeno número de regras fixas, ditas regras básicas, e admite ser estendido com novas regras extra, definidas usando métodos de coerção, como veremos na secção seguinte.
É o seguinte o resumo da presente secção. Na secção 10.2.1, comentamos as regras básicas
do sistema prático, as quais são listadas na secção 10.2.2. Na secção 10.2.3, apresentamos as
regras extra introduzidas na biblioteca padrão da linguagem OM. Na secção 10.2.4, introduzimos um operador explícito de conversão. Nas secções 10.2.5 e 10.2.6 apresentamos diversas
noções ligadas a sistemas de prova e a procedimentos de prova. Finalmente, na secção 10.2.7,
fazemos o levantamento e análise de diversos problemas associados ao sistema natural.
10.2.1 Apresentação das regras básicas do sistema natural
Neste secção, comentamos as regras de coerção básicas do sistema natural. A lista completa
destas regras encontra-se na secção seguinte.
A regra [Coerção ≤] faz com que a relação de subtipo implique a relação de coerção. A função de conversão associada a esta regra é a função identidade com domínio no supertipo.
Sendo a relação de subtipo reflexiva, a relação de coerção também fica reflexiva.
A extensibilidade do sistema natural assenta na regra básica [Coerção extra], a qual assenta na
possibilidade de se definirem métodos de coerção, da forma:
–
–
coercion #f = λX.λarg:ϑ[X]. e
– –
–
:∀X.ϑ[X]→Ω[X]
10 Sistema de coerções
175
Os métodos de coerção são definidos no nível privilegeado da linguagem (cf. secção 11.4).
Cada método de coerção especifica duas entidades em simultâneo: uma regra de coerção
– extra –
–
–
extra, denotada por ϑ[X
]≤
[X], e uma função de conversão, denotada por (ϑ[X]≤ extra[X]) .
. Convencionalmente, consideramos que todas as variáveis de tipo destas expressões estão
quantificadas universalmente. Eis três exemplos de regras extra que podem ser introduzidas
usando métodos de coerção: Int≤extraFloat , X→Bool≤ extraX→GenT X , X→Y≤ extraX→{a:Y}. Como
veremos na secção 10.2.3, as regras extra estão submetidas a uma importante restrição: vistas
como padrões, elas têm de ser disjuntas duas a duas.
A regra [Coerção nname], com ajuda da função de conversão que lhe está associada, suporta a
transformação sintáctica exp-->λz:Unit.exp. Esta é uma forma simples de tirar partido do sistema
de coerções para implementar a estratégia de avaliação call-by-name. Note que, não obstante a
regra [Coerção nname] ter a forma aproximada duma regra extra, não é possível substitui-la pela
regra extra X≤extraUnit→X. Se tal fosse feito, o correspondente método de coerção seria
coercion #name = λX.λarg:X. (λz:Unit. arg)
:∀X.X→(Unit→X)
mas é fácil de ver que a função de conversão que este método define não tem qualquer utilidade, já que o argumento arg é avaliado prematuramente em virtude da estratégia de avaliação
call-by-value de L10. Dentro da biblioteca padrão da linguagem OM, esta regra é usada nas
seguintes situações: na atribuição de semântica aos comandos da linguagem; na atribuição de
semântica à expressão condicional ?:; na definição dos métodos '&&' e '||' , na classe Bool; na
definição de diversos métodos dentro dos modos lazy e gen.
A regra [Coerção →] adapta a regra [Sub →] ao contexto do sistema de coerções. É uma regra
que, na prática, tende a ser usada apenas como regra auxiliar pela regra [Coerção {…}]. Um raro
exemplo em usamos a regra [Coerção →] de forma mais substancial encontra-se na redefinição
do método #$def_apply, no modo gen da biblioteca padrão: o primeiro parâmetro de #$def_apply é
deliberadamente declarado com o tipo GenT (τ→(GenT σ)), para que possa aceitar argumentos do
largo espectro de tipos pretendido. Considera-se que o tipo GenT (τ→(GenT σ)) é muito geral
porque ele surge do lado direito de todos os seguintes juízos de coerção, cuja validade é fácil
de provar com a ajuda da regra [Coerção →]:
Γ
Γ
Γ
Γ
(GenT τ)→σ
τ→σ
(GenT τ)→(GenT σ)
τ→(GenT σ)
≤c GenT (τ→(GenT σ))
≤c GenT (τ→(GenT σ))
≤c GenT (τ→(GenT σ))
≤c GenT (τ→(GenT σ))
A regra [Coerção {…}] adapta ao contexto do sistema de coerções a regra [Sub {…}] da relação de subtipo. Na prática, só se recorre a esta regra para ajudar a resolver o problema da
instanciação duma entidade paramétrica com um tipo que, não sendo imediatamente compatível com a sua interface-limite, possa ser tornado compatível com ela por acção duma coerção
(cf. 6.3.2 e 6.3.2.2).
176
OM – Uma linguagem de programação multiparadigma
Para terminar, a regra [Coerção trans] serve para tornar transitiva a relação binária de coerção.
A função de conversão associada ao juízo resultante obtém-se através da composição das funções de conversão associadas aos juízos envolvidos na transitividade.
10.2.2 Regras básicas do sistema natural
O sistema natural é um sistema de prova que axiomatiza uma relação de coerção sobre os tipos
da espécie ∗, em L10. As regras do sistema natural estão definidas sobre juízos de coerção da
forma descrita na secção 10.1.2. Cada uma das regras do sistema tem uma função de conversão associada, definida no mesmo contexto Γ do juízo que ocorre na conclusão da regra.
O sistema natural inclui três regras terminais (cf. definição 10.2.5-1): [Coerção ≤],
[Coerção extra] e [Coerção nname] .
Eis a lista integral das regras básicas que definem o sistema natural:
[Coerção ≤]
Γ υ≤τ Γ τ:∗
Γ υ≤cτ
(Γ υ≤cτ)
[Coerção extra]
–
–
–
ϑ[X]≤ extraΩ[X] Γ σ:∗
–
–
Γ ϑ[σ]≤ cΩ[σ]
–
–
(Γ ϑ[σ]≤ cΩ[σ])
[Coerção nname]
Γ τ:∗
Γ τ≤ cUnit→τ
(Γ τ≤ cUnit→τ) exp
[Coerção →]
Γ υ′≤ cυ Γ τ≤ cτ′
Γ υ→τ≤ cυ′→τ′
(Γ υ→τ≤ cυ′→τ′) =ˆ
λf:υ→τ. λx:υ′. (Γ τ≤ cτ′) (f ((Γ υ′≤ cυ)x))
=ˆ λx:υ. (λy:τ.y)x
=ˆ (ϑ[σ–]≤ extraΩ[σ–])
=ˆ λz:Unit.exp
[Coerção {…}]
Γ τ1 ≤cτ1 ′ … Γ τk ≤cτk ′
Γ {l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }≤c{l 1 :τ1 ′‚…‚l k :τk ′}
(Γ {l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }≤c{l 1 :τ1 ′‚…‚l k :τk ′}) =ˆ
λr:{l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }.
{l 1 =(Γ τ1 ≤cτ1 ′) rl1 ‚…‚lk =(Γ τk ≤cτk ′) r.lk }
[Coerção trans]
Γ τ≤ cτ′ Γ τ′≤cτ′′
Γ τ≤ cτ′′
(Γ τ≤ cτ′′)
=ˆ λx:τ. (Γ τ′≤cτ′′)(Γ τ≤ cτ′)x
10.2.3 Regras de coerção extra
Já vimos, na secção 10.2.1, que a regra básica [Coerção extra] permite estender o sistema natural
com regras extra e respectivas funções de conversão. Ao nível do nosso sistema de coerções
formal, as regras extra têm a forma geral
–
–
ϑ[X]≤ extraΩ[X]
10 Sistema de coerções
177
Nos programas, as regras extra são denotadas usando métodos de coerção.
Introduzimos a seguinte restrição de disjunção para eliminar o indeterminismo na aplicação
de regras extra e o consequente problema de ambiguidade ligado à existência de funções de
conversão de aplicação alternativa:
Restrição 10.2.3-1 (Restrição de disjunção) Vistas como padrões, as regras extra têm
de ser disjuntas duas a duas.
Exemplificando, as regras X≤extraConstT X e Y&≤ extraY obedecem à restrição de disjunção
porque não existe qualquer par de substituições que as torne iguais. No entanto, as regras
X≤ extraInt→X e Y≤ extraY→Int já não obedecem à restrição pois existe um par de substituições,
[Int/X ] e [Int/Y], que as tornam iguais. Repare que o juízo Γ Int≤ cInt→Int poderia ser deduzido a
partir de qualquer delas com a ajuda da regra [Coerção extra].
Note que esta restrição de disjunção elimina o indeterminismo apenas ao nível das regras
extra; mas não o elimina ao nível do sistema natural completo, como veremos na secção
10.2.7.1. A introdução da restrição de disjunção justifica-se apenas sua contribuição para a definição dos dois procedimentos de prova deterministas que serão introduzidos na secção 10.3.
Apresentamos agora a lista completa das regras de coerção extra incluídas na versão corrente da biblioteca padrão da linguagem OM.
X≤ extraConstT X
ConstT X≤ extraX
Int≤ extraFloat
X≤ extraValueT X
ValueT X≤extraX
Ref X≤ extraX
Unit→X≤extraLazyT X
LazyT X≤extraX
X≤ extraLogT X
LogT X≤extraGenT X
X→Bool≤extraX→GenT X
Unit→X≤extraGenT X
Na definição da biblioteca padrão da linguagem, nos capítulos 11 e 12, podem ser consultados os métodos de coerção usados para introduzir estas regras e as respectivas funções de conversão.
10.2.4 Operadores de conversão
As regras de coerção influenciam o significado dos programas de forma implícita. Com efeito,
em cada aplicação duma coerção a uma expressão, a invocação da correspondente função de
conversão é inserida automaticamente pelo compilador durante a fase de análise semântica.
Contudo, por razões de clareza, ênfase, ou então para ultrapassar alguma limitação do sistema
de coerções, por vezes seria conveniente que existisse a possibilidade de aplicar uma coerção a
uma expressão de forma explícita.
Assim, para permitir conversões explícitas, introduzimos na linguagem L10 uma família de
operadores de conversão, sintáctica e semanticamente idênticos aos operadores de cast da lin-
178
OM – Uma linguagem de programação multiparadigma
guagem C. Estes operadores têm a forma “(τ)” onde τ é um tipo. Para exemplificar, a expressão
(1+x) pode ser explicitamente convertida numa expressão do tipo Float , aplicando-lhe o operador conversão (Float), ou seja escrevendo (Float)(1+x) .
A introdução dos operadores de conversão é formalizada usando a seguinte regra:
[Termo conversão]
Γ e:τ Γ τ≤ cτ′
Γ (τ′)e:τ′
A equação semântica do novo termo (τ′)e é a seguinte:
(τ′)e :τ′
=ˆ let E:τ = e in (τ≤cτ′)E
10.2.5 Regras terminais e árvores de prova
Nesta secção adaptamos ao contexto dos sistemas de coerções algumas noções importantes ligadas a sistemas de prova.
Definição 10.2.5-1 (Regra terminal) Num sistema de prova sobre juízos duma dada
forma, uma regra diz-se terminal se nas suas premissas não ocorrerem juízos dessa mesma forma. Nas premissas duma regra terminal podem ocorrer apenas juízos doutras formas, portanto
associados a outros sistemas de prova.
Para exemplificar, no sistema natural as regras terminais são três: [Coerção ≤] , [Coerção extra]
e [Coerção nname]. Estas regras são exactamente aquelas em cujas premissas não ocorrem juízos
– extra –
da forma Γ υ≤cτ; nelas ocorrem apenas juízos da forma Γ υ≤τ , Γ τ: ∗ e ϑ[X
]≤
[X].
Definição 10.2.5-3 (Juízo válido) Num sistema de prova, um juízo diz-se válido se for
possível construir para ele uma árvore de prova cujas folhas sejam todas premissas válidas de
instâncias de regras terminais. Um juízo diz-se inválido se não for válido.
Para exemplificar, vejamos uma árvore de prova que mostra que, no sistema natural, o juízo
Γ Float→Ref Int≤ cRef Int→Float é válido. Na construção da árvore usamos as regras básicas
[Coerção →], [Coerção trans], [Coerção extra] e, ainda, as regras extra Ref X≤ extraX e Int≤extraFloat .
Ref X≤ extraX Γ Int:∗ Int≤ extraFloat Ref X≤ extraX Γ Int:∗ Int≤ extraFloat
Γ Ref Int≤ cInt
Γ Int≤ cFloat
Γ Ref Int≤ cInt
Γ Int≤ cFloat
c
c
Γ Ref Int≤ Float
Γ Ref Int≤ Float
c
Γ Float→Ref Int≤ Ref Int→Float
Uma árvore de prova, construída para um juízo de coerção válido, determina automaticamente a função de conversão que se associa a esse juízo. Essa função é construída indutivamente, combinando as várias funções de conversão associadas às regras envolvidas na prova.
Para exemplificar, a função de conversão correspondente à árvore de prova anterior é:
10 Sistema de coerções
179
(Γ Float→Ref Int≤ cRef Int→Float) =ˆ
λf:Float→Ref Int. λx:Ref Int. (Γ Ref Int≤ cFloat) (f ((Γ Ref Int≤ cFloat)x))
onde:
(Γ Ref Int≤ cFloat) =ˆ λx:Ref Int. (Γ Int≤ cFloat)(Γ Ref Int≤ cInt)x
(Γ Ref Int≤ cInt) =ˆ (Ref Int≤extraInt)
(Γ Int≤ cFloat) =ˆ (Int≤extraFloat)
Repare que a estrutura da função de conversão segue fielmente a estrutura da árvore de prova.
A noção seguinte será usada na definição do procedimento de prova prático (cf. definição
10.3.4-2).
Definição 10.2.5-4 (Tamanho duma árvore de prova) No contexto dum sistema de
prova sobre juízos da forma Γ υ≤cτ, chama-se tamanho duma árvore de prova ao número de
nós da forma Γ υ≤cτ que integram essa árvore de prova.
Como é fácil de verificar, o tamanho duma árvore de prova corresponde ao número de nós internos dessa árvore de prova.
10.2.6 Procedimentos de prova
Como é que se constrói uma árvore de prova para um juízo válido? A forma mais directa
baseia-se na aplicação directa das regras do sistema de prova. Partindo do juízo que se pretende provar, tenta-se construir gradualmente uma árvore de prova para ele, usando as regras do
sistema ao contrário, ou seja da conclusão para as premissas, até se alcançarem instâncias de
regras terminais. Depois, separadamente, estabelece-se a validade das premissas dessas instâncias de regras terminais nos respectivos sistemas de prova.
A árvore de prova apresentada na secção anterior foi por nós construída usando este método. Primeiro tivemos de descobrir que o juízo a provar coincidia com a conclusão duma instância da regra [Coerção →] ; este facto permitiu a criação da primeira ramificação da árvore de
prova. Depois, recursivamente, tentámos construir subárvores de prova para as premissas dessa instância de regra, concretamente para os juízos Γ Ref Int≤ cFloat e Γ Ref Int≤ cFloat (estes são
iguais por coincidência). Continuámos a proceder desta forma até alcançarmos as folhas da
árvore. Finalmente, verificámos separadamente a validade das folhas da árvore, ou seja dos
juízos Γ Int:∗, X≤ extraX, e Int≤extraFloat .
Esboçado um procedimento de prova baseado na utilização directa das regras do sistema de
prova, vamos agora considerar a forma como ele pode ser programado. Comecemos por observar que o procedimento acima descrito lida com instâncias de regras, ou seja com substituições. Por esse motivo, é razoável pensar que o procedimento será mais facilmente programado
numa linguagem que suporte emparelhamento de padrões ou unificação, do que numa outra
linguagem mais convencional.
180
OM – Uma linguagem de programação multiparadigma
A programação do procedimento ficará também facilitada se todas as regras do sistema forem sintacticamente dirigidas [Pie93], isto é se verificarem a propriedade: “todas as variáveis
que ocorrem nas premissas ocorrem também na conclusão”. Neste caso é possível escrever o
procedimento de prova usando apenas emparelhamento de padrões.
Contudo, basta que no sistema de prova exista alguma regra que não seja sintacticamente
dirigida para que passe a ser necessário usar variáveis simbólicas e unificação estrutural. O
sistema natural inclui uma destas regras problemáticas: a regra [Coerção trans] cujas premissas
contêm uma variável, τ′, que não ocorre na conclusão:
[Coerção trans]
Γ τ≤ cτ′ Γ τ′≤cτ′′
Γ τ≤ cτ′′
Um procedimento de prova que aplique esta regra da conclusão para as premissas tem necessidade de inventar um tipo intermédio τ′, o qual deverá ser representado por uma variável
simbólica.
Feita esta introdução ao tema dos procedimentos de prova, apresentamos seguidamente
algumas definições importantes relacionadas com este assunto.
Em certos sistemas de prova, quando se tenta provar um juízo usando o método atrás descrito pode acontecer que este emparelhe com as conclusões de diversas regras. Por exemplo,
no caso do sistema natural, o juízo Γ Int→Int≤ cInt→Int emparelha com as conclusões das quatro
regras: [Coerção ≤], [Coerção →] , [Coerção trans] e [Coerção extra] . A forma como um procedimento
de prova concreto lida com este tipo de situação, distingue os procedimentos deterministas dos
procedimentos indeterministas.
Definição 10.2.6-1 (Procedimento de prova determinista) Um procedimento de
prova diz-se determinista se tiver embutida uma estratégia de exploração sequencial das
regras do sistema de que resulte, em cada passo, a escolha duma delas, a usar na construção
duma árvore de prova.
Um procedimento de prova diz-se indeterminista se tiver a capacidade de explorar as regras
do sistema em paralelo, durante a construção duma árvore de prova.
(Estas duas definições são derivadas das definições de procedimento determinista e procedimento indeterminista de [Rèz85] págs 108,109).
Definição 10.2.6-2 (Sucesso/falhanço duma regra) Dado juízo que se pretende provar, diz-se que uma regra r sucede se o procedimento de prova conseguir construir, para esse
juízo, uma árvore de prova com uma instância de r na raiz. Caso contrário, diz-se que a regra r
falha.
A título de exemplo, um conhecido procedimento de prova determinista explora as regras
que compõem o sistema pela ordem de ocorrência e escolhe a primeira regra que suceder (se
10 Sistema de coerções
181
alguma suceder) para aplicação efectiva. Este é o procedimento de prova determinista que será
introduzido na definição 10.3.4-1. Um exemplo de procedimento de prova indeterminista é
apresentado na definição 10.2.7.3.1-1.
Um procedimento de prova, determinista ou indeterminista, pode ser incapaz de provar alguns dos juízos válidos dum sistema. Por exemplo, isso acontecerá se o procedimento ignorar
deliberadamente algumas regras do sistema. Assim é importante dispor do conceito de procedimento de prova completo:
Definição 10.2.6-3 (Procedimento de prova completo) No contexto dum dado sistema de prova, um procedimento de prova diz-se completo se conseguir provar todos os juízos
válidos desse sistema. Se não o conseguir, o procedimento diz-se incompleto.
A questão da terminação do procedimento de prova é essencial, pois normalmente existe o
objectivo de validar os juízos mecanicamente.
Definição 10.2.6-4 (Algoritmo de prova) Um algoritmo de prova é um procedimento
de prova que comprovadamente termina em todas as circunstâncias, quer seja aplicado a um
juízo válido, quer seja aplicado a um juízo inválido.
Definição 10.2.6-5 (Algoritmo de prova completo) Um algoritmo de prova completo é um procedimento de prova completo que comprovadamente termina em todas as circunstâncias.
10.2.7 Problemas do sistema natural
O sistema natural procura definir de forma simples e intuitiva, ou seja de forma natural, uma
relação binária de coerção entre tipos. No entanto, o sistema tem alguns problemas que requerem análise e a procura de soluções razoáveis. Da discussão e tratamento destes problemas
nascerá o sistema prático, a apresentar na secção 10.3.
Os problemas do sistema natural são três: indeterminismo, ambiguidade, indecidibilidade.
Nesta secção, apenas mostramos que estes problemas existem no sistema natural. Só os tentaremos resolver mais adiante, na secção 10.3.
10.2.7.1 Indeterminismo
Nesta secção, mostramos que o sistema natural é indeterminista. O indeterminismo está na
origem de duas dificuldades: (1) as regras do sistema não definem automaticamente um procedimento de prova determinista; (2) existe ambiguidade na determinação da função de conversão a associar a cada juízo de coerção válido.
182
OM – Uma linguagem de programação multiparadigma
Definição 10.2.7.1-1 (Sistema de prova indeterminista) Um sistema de prova
diz-se indeterminista se existir pelo menos um juízo que possa ser deduzido das suas regras
usando duas árvores de prova distintas. Um sistema de coerções diz-se determinista se todo o
juízo que dele se possa deduzir admitir uma única árvore de prova.
Teorema 10.2.7.1-2 O sistema natural é indeterminista.
Prova: Precisamos apenas de exibir um juízo que admita com duas árvores de prova distintas.
Vamos apresentar outra árvore de prova para o juízo Γ Float→Ref Int≤ cRef Int→Float, alternativa
da que foi apresentada na secção 10.2.5. Na nova árvore, usamos o símbolo “…” para representar uma determinada subárvore de prova que já ocorria na primeira árvore.
Γ Float≤Float Γ Float:∗
…
…
Γ Float≤Float Γ Float:∗
c
c
c
Γ Float≤ Float
Γ Ref Int≤ Float Γ Ref Int≤ Float
Γ Float≤cFloat
Γ Float→Ref Int≤ cFloat→Float
Γ Float→Float≤cRef Int→Float
c
Γ Float→Ref Int≤ Ref Int→Float
Comparando as duas provas, verifica-se que a segunda é mais complicada por incluir uma
aplicação desnecessária da regra [Coerção trans].
Este caso de indeterminismo é apenas um exemplo entre muitos outros possíveis. Na verdade, no sistema natural todo o juízo de coerção válido admite um número infinito de provas distintas. Isso acontece porque as utilização combinada das regras [Coerção ≤] e [Coerção trans]
permite a criação dum número arbitrário de ramificações inúteis em qualquer árvore de prova.
O seguinte exemplo ilustra esta possibilidade:
Γ Int≤Int Γ Int≤Int Γ Int≤Int Int≤ extraFloat
Γ Int≤ cInt Γ Int≤ cInt Γ Int≤ cInt Γ Int≤ cFloat
Γ Int≤ cInt
Γ Int≤ cFloat
c
Γ Int≤ Float
A prova de Γ Int≤ cFloat , que poderia ter sido efectuada de forma directa, usando só a regra
extra Int≤extraFloat , foi antes efectuada duma forma desnecessariamente complicada recorrendo
a três utilizações combinadas inúteis das regras [Coerção ≤] e [Coerção trans].
Existem outros exemplos de indeterminismo que resultam da interacção entre a regra
[Coerção ≤] e uma das duas regras seguintes: [Coerção →] ou [Coerção {…}]. Por exemplo, o juízo
Γ Int→Int≤ cInt→Int pode ser provado de forma imediata através duma aplicação de [Coerção ≤]
ou de forma ligeiramente menos imediata usando [Coerção →].
As regras extra, ao serem conjugadas com as regras básicas, podem também ser fonte de indeterminismo.
As regras extra, entre si, nunca são fonte de indeterminismo, devido à restrição de disjunção, introduzida na secção 10.2.3.
10 Sistema de coerções
183
10.2.7.2 Ambiguidade
Nesta secção, mostramos que o sistema natural é ambíguo. A ambiguidade é um problema que
terá de ser resolvido a todo o custo, pois a boa definição do sistema de coerções depende desse
aspecto.
Definição 10.2.7.2-1 (Sistema de coerções incoerente ou ambíguo) Um sistema
de coerções diz-se incoerente ou ambíguo se admitir que derivações alternativas dum juízo
válido produzam funções de conversão distintas.
Um sistema de coerções diz-se coerente se derivações alternativas dum juízo válido derem
sempre origem à mesma função de conversão.
Um sistema de coerções ambíguo admite a validade de pelo menos uma coerção relativamente à qual não está clarificada qual a função de conversão a usar no momento da sua aplicação.
Teorema 10.2.7.2-2 Todo o sistema de coerções ambíguo é indeterminista. Mas nem todo
o sistema de coerções indeterminista é ambíguo.
Prova: A primeira proposição resulta imediatamente da definição de sistema de coerções
ambíguo, pois uma condição prévia para um sistema ser ambíguo é a possibilidade de derivar
um juízo usando duas árvores de prova distintas.
A segunda proposição afirma que existem sistemas indeterministas coerentes, sistemas em
que existem juízos deriváveis usando duas ou mais árvores de prova distintas, mas a cada uma
dessas árvores está sempre associada a mesma função de conversão. O teorema 10.3.5-5 apresentará um sistema com estas características.
Teorema 10.2.7.2-3 O sistema natural é ambíguo.
Prova: Precisamos apenas de exibir um exemplo de ambiguidade no sistema natural. Já que a
regra [Coerção extra] permite introduzir regras extra, podemos introduzir uma dessas regras que
se sobreponha a uma regra básica. Vamos introduzir uma regra extra que se sobrepõe parcialmente à regra básica [Coerção ≤].
Assim, seja {a:Int, b:Int}≤ extra{a:Int} uma nova regra extra, com a seguinte função de conversão associada:
({a:Int, b:Int}≤ extra{a:Int})
=ˆ λx:{a:Int,b:Int}.{a=x.a+1}
Com a introdução desta regra, passam a haver pelo menos duas formas diferentes de derivar a
asserção {a:Int, b:Int}≤ c{a:Int}: uma aplicando directamente a nova regra extra com função de
conversão: λx:{a:Int, b:Int}.{a=x.a+1} ; outra aplicando directamente a regra [Coerção ≤], com função de conversão: λx:{a:Int, b:Int}. (λy:{a:Int}.y)x. Como as funções de conversão são diferentes,
conclui-se que o sistema natural é ambíguo.
184
OM – Uma linguagem de programação multiparadigma
10.2.7.3 Indecidibilidade
Na linguagem L10, as coerções deverão ser validadas mecanicamente antes de serem aplicadas
pelo compilador. Assim, existe a necessidade de definir um algoritmo de prova que verifique a
derivabilidade de juízos de coerção no sistema natural.
Na secção corrente, veremos que não é possível definir um tal algoritmo para o sistema
natural. Na secção 10.3, introduziremos uma forma pragmática de ultrapassar este problema.
10.2.7.3.1 Procedimento geral de prova
O procedimento geral de prova é um procedimento de prova indeterminista que consegue
construir uma árvore de prova todo o juízo válido, no contexto de qualquer sistema de prova.
Infelizmente só se garante a terminação deste procedimento nos casos em que ele é aplicado a
juízos válidos. O que sucede quando ele é aplicado a um juízo inválido depende das regras
particulares que constituem o sistema e ainda do juízo particular em análise.
Definição 10.2.7.3.1-1 (Procedimento geral de prova) Tomando como ponto de
partida o juízo que se pretende provar, o procedimento geral de prova constrói incrementalmente e explora sistematicamente todas as potenciais árvores de prova com essa conclusão, até
atingir uma das duas situações: (1) ou encontra um árvore de prova que valida o juízo; (2) ou
conclui que uma tal árvore não existe, sendo o juízo inválido. No primeiro caso diz-se que o
procedimento termina com sucesso, e no segundo caso que o procedimento termina com insucesso.
São os seguintes os principais detalhes da gestão das árvores. Quando uma folha duma árvore pode ser desenvolvida por aplicação de n≥1 diferentes regras do sistema, o procedimento
cria n-1 novos exemplares dessa árvore e expande cada uma das n árvores iguais usando uma
regra distinta. Sempre que uma folha duma árvore não pode ser desenvolvida por aplicação de
qualquer regra do sistema, se essa folha não for uma premissa válida duma regra terminal então a respectiva árvore é destruída. Se todas as árvores forem destruídas então o procedimento
termina com insucesso.
O procedimento explora as potenciais árvores de prova sistematicamente: primeiro tentando encontrar uma árvore de altura 1 que prove o juízo; depois, se necessário, expandindo um
pouco mais as árvores existentes para tentar encontrar uma árvore de altura 2 que prove o juízo; e assim sucessivamente.
Teorema 10.2.7.3.1-2 (Completitude do procedimento geral de prova) O procedimento geral de prova consegue construir, em tempo finito, uma árvore de prova para qualquer juízo válido em qualquer sistema de prova. Por outras palavras, o procedimento geral de
prova é completo no contexto de qualquer sistema de prova. No entanto, o procedimento pode
10 Sistema de coerções
185
não terminar quando aplicado a um juízo inválido. No caso particular do sistema natural, o
procedimento geral de prova nunca termina quando é aplicado a um juízo inválido.
Prova:
1ªparte: Todo o juízo válido admite uma árvore de prova ψ de altura finita. Ora o procedimento geral de prova analisa as possíveis árvores de prova de forma sistemática, tentando primeiro
encontrar uma com altura 1, depois uma com altura 2, etc. Desta forma só há duas alternativas
possíveis: ou o procedimento descobre a árvore ψ atrás referida, ou então o procedimento descobre uma outra árvore de prova de altura não-superior à altura de ψ.
2ª e 3ª partes: No caso do sistema natural, a regra [Coerção trans] é sempre aplicável em qualquer subprova, pelo que o procedimento geral de prova nunca destrói uma árvore previamente
criada. Por outro lado, um juízo inválido não admite qualquer árvore de prova. Desta forma,
nunca se chega a verificar qualquer das duas condições de paragem.
10.2.7.3.2 Propriedade da subfórmula
Na procura dum procedimento de prova para o sistema natural que termine, ou seja dum algoritmo de prova, surge naturalmente a ideia da propriedade da subfórmula: Precisamos de introduzir esta conhecida propriedade numa formulação mais geral do que a que se encontra, por
exemplo, em [Cas98].
Definição 10.2.7.3.2-1 (Propriedade da subfórmula) Consideremos um sistema de
prova sobre juízos da forma Γ υθτ, para um símbolo de relação θ arbitrário. Seja r uma regra
genérica desse sistema de prova com conclusão Γ′ υ′θτ′. Diz-se que a regra r satisfaz a propriedade da subfórmula se todas as premissas de r que tiverem a forma Γ υθτ verificarem as duas
condições seguintes: (1) υ e τ são subfórmulas em sentido lato de υ′ e τ′, respectivamente; (2)
pelo menos uma das duas componentes, υ ou τ, é subfórmula em sentido estrito de υ′ ou τ′, respectivamente.
Esta propriedade será explorada na definição do sistema prático, na secção 10.3. Note, desde já, que todas as regras terminais de qualquer sistema satisfazem vacuamente a propriedade
da subfórmula.
Teorema 10.2.7.3.2-2 Se todas as regras básicas dum sistema de prova satisfizerem a propriedade da subfórmula, então qualquer procedimento de prova baseado na utilização dessas
regras é um algoritmo. (Assume-se que já existem algoritmos para provar a validade das premissas das regras terminais).
Prova: A relação de subfórmula estrita “ >f”, definida no conjunto das fórmulas, é uma relação
de ordem parcial estrita bem fundada, onde portanto não existem sequências descendentes infinitas f 1 >ff2 > f…. Se o procedimento de prova não terminasse então seriam criadas cadeias descendentes infinitas.
186
OM – Uma linguagem de programação multiparadigma
Note que este teorema é válido para qualquer procedimento baseado na aplicação das regras
dum sistema de prova,. Mesmo no caso dum procedimento incompleto se continua a garantir a
sua terminação, apesar dele não conseguir provar todos os juízos válidos do sistema.
Analisando o sistema natural, verifica-se que ele inclui três regras terminais – [Coerção ≤],
[Coerção extra] e [Coerção nname] – as quais já verificam a propriedade da subfórmula. Das três
regras não-terminais, as regras [Coerção →] e [Coerção {…}] também verificam a propriedade da
subfórmula.
Apenas a regra não-terminal [Coerção trans] não verifica essa propriedade. Por isso importa
averiguar se é viável transformar o sistema natural num sistema equivalente, trocando a regra
não-terminal [Coerção trans] por outras regras não-terminais que satisfaçam a propriedade da
subfórmula. Infelizmente tal não é possível, como mostra o seguinte teorema.
Teorema 10.2.7.3.2-3 Consideremos uma variante do sistema natural onde a regra da transitividade foi sido substituída por uma colecção de regras não-terminais satisfazendo, todas
elas, a propriedade da subfórmula. Nesta situação, o sistema natural e sua variante não são
equivalentes, o que significa que os dois sistemas definem relações distintas.
Prova: Vamos considerar os tipos atómicos Bool, Int e Float e assumir que no sistema foram introduzidas apenas as duas seguintes regras extra: Bool≤extraInt e Int≤ extraFloat . Nestas circunstâncias, no contexto do sistema natural, a regra da transitividade permite provar Γ Bool≤ cFloat ,
como mostra a árvore de prova:
Bool≤ extraInt Int≤ extraFloat
Γ Bool≤ cInt Γ Int≤ cFloat
Γ Bool≤ cFloat
Vamos considerar agora o sistema modificado que resulta da concretização da troca de regras indicada no enunciado do teorema. No contexto do novo sistema, vamos determinar se
existe alguma regra que possa ser usada em último lugar numa dedução de Γ Bool≤ cFloat :
…
Γ Bool≤ cFloat
Começamos por mostrar que as três regras terminais não podem ser usadas naquela posição: a regra [Coerção ≤] não é aplicável pois o juízo de subtipo Γ Bool≤Float não é válido em F +;
a regra [Coerção extra] não é aplicável pois não existe a regra extra Bool≤extraFloat ; a regra
[Coerção nname] também não é aplicável devido à estrutura da conclusão desta regra.
Analisamos agora as regras não-terminais: todas elas obedecem à propriedade da subfórmula, pelo que o juízo Γ Bool≤ cFloat terá de ser provado à custa das subfórmulas de Bool e das subfórmulas de Float, sendo que em pelo menos um destes casos é necessário considerar subfórmulas estritas. Mas os tipos Bool, Float são atómicos, o que significa que não têm subfórmulas
estritas. Assim não existe qualquer regra não-terminal que possa ser aplicada em último lugar
na dedução pretendida.
10 Sistema de coerções
187
Conclui-se que o juízo Γ Bool≤ cFloat , válido no sistema natural, é inválido no sistema modificado, o que significa que o novo sistema não é equivalente ao sistema natural.
No teorema anterior investigámos a possibilidade de construir um sistema de prova equivalente ao sistema natural, trocando da regra da transitividade por um conjunto de regras não-terminais, satisfazendo a propriedade da subfórmula. O resultado foi negativo, e, infelizmente, a situação também não melhora se passarmos a admitir a introdução de novas regras terminais. É o que refere o teorema seguinte, cuja demonstração antecipa já um resultado da próxima secção:
Teorema 10.2.7.3.2-4 Não existe qualquer sistema de prova cujas regras básicas satisfaçam a propriedade da subfórmula e seja equivalente ao sistema natural.
Prova: Se um tal sistema de prova existisse então ele admitiria um algoritmo de prova (cf. teorema 10.2.7.3.2-2). Mas esse seria também um algoritmo de prova para a relação de coerção
definida pelo sistema natural, e acontece que tal algoritmo não pode existir (cf. teorema
10.2.7.3.3-4, da próxima secção). Logo, esse sistema de prova alternativo não pode existir.
10.2.7.3.3 Indecidibilidade do sistema natural
Nesta secção vamos considerar a seguinte questão: “Será que existe algum algoritmo, qualquer
algoritmo, baseado ou não em regras, que permita verificar a relação de coerção definida pelo
sistema natural?” Por outras palavras: “Será que a relação de coerção definida pelo sistema natural é decidível?”.
Como já se terá apercebido o leitor mais observador, a relação de coerção definida pelo sistema natural é de facto indecidível. Isso deve-se a três razões: o sistema natural é extensível;
as regras extra têm um formato muito geral; existe uma regra da transitividade que permite sequenciar livremente a aplicação das regras extra. Realmente, estas três razões fazem com que o
sistema natural encerre em si o poder computacional duma linguagem de programação universal, donde o problema da verificação da relação de coerção definida por ele será forçosamente
equivalente ao problema da paragem (halting problem), um conhecido problema indecidível
[HU79].
A demonstração de que a relação de coerção definida pelo sistema natural é indecidível é
em grande parte rotineira. É apenas para sermos completos que a vamos apresentar, embora de
forma abreviada. Iremos mostrar que toda a máquina de dois contadores T [HU79] pode ser
codificada como um conjunto de regras extra, de tal forma que a máquina T pára sse um determinado juízo de coerção for derivável no sistema natural.
Definição 10.2.7.3.3-1 (Máquina de dois contadores) [Definição ligeiramente adaptada de Pierce [Pie93] que por sua vez simplifica a definição do livro de Hopcroft e Ullman
[HU79]]. Uma máquina de dois contadores é um quádruplo ordenado (PC,A0 ,B0 ,I1 …Iw), onde
188
OM – Uma linguagem de programação multiparadigma
é uma instrução, A0 e B0 são números naturais, e I1 …Iw, é uma sequência de instruções etiquetadas. Há cinco formas de instruções: INCA⇒m , INCB⇒m , TSTA⇒m/n, TSTB⇒m/n, HALT .
Cada forma de instrução provoca um tipo diferente de transição entre máquinas, de acordo
com a seguinte tabela:
PC
INCA⇒m
INCB⇒m
TSTA⇒m/n
TSTB⇒m/n
(INCA⇒m,A,B,I1 …Iw)
(INCB⇒m,A,B,I1 …Iw)
(TSTA⇒m/n,0,B,I1 …Iw)
(TSTA⇒m/n,A,B,I1 …Iw)
(TSTB⇒m/n,A,0,I1 …Iw)
(TSTB⇒m/n,A,B,I1 …Iw)
(HALT,A,B,I1 …Iw)
HALT
===> (Im,A+1,B,I1 …Iw)
===> (Im,A,B+1,I1 …Iw)
===> (Im,0,B,I1 …Iw)
===> (In,A-1,B,I1 …Iw)
===> (Im,A,0,I1 …Iw)
===> (In,A,B-1,I1 …Iw)
===> indefinido
se A≠0
se B≠0
Note que as instruções TSTA e TSTB são instruções que, simultaneamente, testam e decrementam um contador.
Definição 10.2.7.3.3-2 (Paragem duma máquina) Uma máquina de dois contadores
* (HALT,A′,B′,I …I ), ou seja se existir um n tal que
pára se (PC,A 0 ,B0 ,I1 …Iw) ===>
1
w
n
(PC,A0 ,B0 ,I1 …Iw) ===> (HALT,A′,B′,I1 …Iw).
(PC,A,B,I1 …Iw)
Teorema 10.2.7.3.3-3 (Indecidibilidade do problema da paragem) Não existe
qualquer algoritmo que possa ser aplicado a qualquer máquina de dois contadores e decida se
ela pára ou não pára.
Prova: Ver Pierce [Pie93] e Hopcroft e Ullman [HU79].
Teorema 10.2.7.3.3-4 (Indecidibilidade do sistema natural) A relação de coerção
definida pelo sistema natural é indecidível, i.e. não existe qualquer algoritmo que verifique a
relação definida pelo sistema natural.
Prova: Vamos mostrar como toda a máquina de dois contadores T≡(I0 ,A0 ,B0 ,I1 …Iw) pode ser
codificada como um conjunto de regras extra de tal forma que a máquina T pára sse a coerção
C[Bool,Bool,A0 ,B0 ]≤ cFloat for derivável no sistema natural. Nesta prova assumimos que não existem regras extra definidas à partida.
Em primeiro lugar, temos de inventar uma codificação para os números naturais usando a
linguagem dos tipos de F+. Utilizaremos a seguinte codificação:
. :Nat→F +
0 =ˆ Bool
n+1 =ˆ Bool→ n
n>0
Dada uma máquina T≡(I0 ,A0 ,B0 ,I1 …Iw) qualquer, codificamos agora as suas instruções usando regras extra. Traduzimos cada instrução I k da sequência I0 I1 …Iw numa regra extra ou em
duas regras extra, de acordo com a seguinte tabela de conversão:
10 Sistema de coerções
189
(INCA⇒m)k
(INCB⇒m)k
(TSTA⇒m/n)k
--->
--->
--->
(TSTB⇒m/n)k
--->
HALT k
--->
C[I, k, X, Y]
C[I, k, X, Y]
C[I, k, Bool, Y]
C[I, k, Bool→X, Y]
C[I, k, X, Bool]
C[I, k, X, Bool→Y]
C[I, k, X, Y]
≤extra
≤extra
≤extra
≤extra
≤extra
≤extra
≤extra
C[Bool→I,
C[Bool→I,
C[Bool→I,
C[Bool→I,
C[Bool→I,
C[Bool→I,
Float
m, Bool→X, Y]
m, X, Bool→Y]
m, Bool, Y]
n, X, Y]
m, X, Bool]
n, X, Y]
Nestas regras extra as letras I, X e Y representam variáveis de tipo implicitamente quantificadas, e C, k, m e n são entidades assim definidas:
C[I,K,X,Y]
k =ˆ k
m =ˆ m
n =ˆ n
=ˆ K→X→Y→I
Note que com a ajuda da regra da transitividade, este conjunto de regras extra define uma
subrelação de coerção cujos pares só podem ser provados usando estas mesmas regras (e quanto muito, usando de forma vácua a regra [Coerção ≤]). Realmente, para cada par da subrelação,
não é possível construir qualquer prova alternativa que seja diferente de forma substancial,
pois as regras extra têm todas o formato C[I,…]≤ extraC[Bool→I,…] e acontece que o juízo
Γ I≤cBool→I é inválido para todo o tipo I e para todo o contexto Γ (lembramos que estamos a
assumir que não existia qualquer regra extra definida à partida).
Uma propriedade do sistema natural, que resulta da tradução acima, é a seguinte:
(I k ,A,B,I1 …Iw) ===> (I m,A′,B′,I1 …Iw) sse ∅ C[0, k, A, B] ≤c C[1, m, A′, B′]
Outra propriedade é a seguinte:
n (I ,A′,B′,I …I ) sse ∅ C[0, k, A, B] ≤c C[n, m, A′, B′]
(I k ,A,B,I1 …Iw) ===>
m
1
w
Em particular:
n
(I 0 , A0 , B 0 ,I1 …Iw) ===>(HALT
m,A′,B′,I1 …Iw)
sse ∅ C[0, 0, A 0 , B 0 ] ≤c C[n, m, A′,B′] e ∅ C[n, m, A′,B′] ≤c Float
sse ∅ C[Bool, Bool, A 0 , B 0 ] ≤c Float
como pretendíamos.
10.3 O sistema prático
No estudo de sistemas de tipos, a regra da transitividade é uma conhecida fonte de problemas
(ver [CG92], por exemplo). Já contactámos dois desses problemas: no sistema natural, a regra
da transitividade é uma fonte de indeterminismo (cf. secção 10.2.7.1) e também uma fonte de
indecidibilidade (cf. teorema 10.2.7.3.3-4),.
190
OM – Uma linguagem de programação multiparadigma
Sem perderem poder de prova, alguns sistemas de tipos admitem que a regra da transitividade seja eliminada, ou, pelo menos, substituída por outras regras menos problemáticas. Um
caso em que se prova ser possível a eliminação pura e simples da regra da transitividade é discutido em [Cas98] págs 36, 37. Um caso em que se tenta substituir a regra da transitividade do
sistema F≤ por uma regra mais fraca é investigado no trabalho [CG92] de Curien e Ghelli. No
entanto, esse enfraquecimento realmente não resolveu o problema, pois, como mostrou Pierce
em [Pie93], o sistema F≤ sofre dum problema fundamental de indecidibilidade.
Essencialmente, será trabalhando sobre a regra da transitividade que ultrapassaremos o problema da indecidibilidade do sistema natural. Iremos introduzir um sistema modificado, chamado sistema prático, no qual a regra da transitividade será eliminada, tomando o seu lugar
três novas regras que já verificam a propriedade da subfórmula (cf. secções 10.3.1 e 10.3.3).
Estas três regras novas cobrem pragmaticamente os casos particulares de transitividade em que
estamos mais interessados, com destaque para as coerções de modo (cf. 10.1.3). A descoberta
destas três regras foi crítica para a viabilização do nosso sistema de coerções extensível.
O sistema prático tem um poder inferior de prova relativamente ao sistema natural (cf. teorema 10.3.6-4). Fatalmente, a solução do problema da indecidibilidade do sistema natural teria
de passar por um compromisso deste tipo: a indecidibilidade é um problema que não se resolve: quanto muito contorna-se através da definição duma outra relação, parecida com a original,
mas não absolutamente igual.
No contexto do sistema prático, estudamos dois procedimentos de prova deterministas distintos. O procedimento de prova prático incorpora o requisito, discutido na secção 10.1.4, da
geração duma árvore de prova com tamanho mínimo. É por isso que, no final, este será o procedimento de prova oficial adoptado para o sistema prático.
Na secção 10.3.1, comentamos todas as regras básicas do sistema prático, cuja lista completa é apresentada na secção 10.3.2. Na secção 10.3.3, discutimos as consequências da eliminação da regra da transitividade no sistema prático. Na secção 10.3.4, apresentamos os procedimentos de prova normalizado e prático. Nas secções 10.3.5 e 10.3.6 provamos algumas propriedades destes procedimentos de prova.
10.3.1 Apresentação das regras básicas do sistema
prático
O sistema prático obtém-se a partir do sistema natural, trocando duas das regras, [Coerção trans]
e [Coerção nname], pelas três novas regras [Coerção name], [Coerção extra_despro], [Coerção extra_pro].
A regra [Coerção name] do sistema prático, resulta da combinação das duas regras do sistema
natural [Coerção trans] e [Coerção nname] (cf. demonstração do teorema 10.3.6-4). Portanto, a nova
regra [Coerção name] resulta mais poderosa do que a regra [Coerção nname]
10 Sistema de coerções
191
As regras [Coerção extra_despro] e [Coerção extra_pro] resultam da combinação da regra da
transitividade com dois casos particulares da regra [Coerção extra] (cf. demonstração do teorema
10.3.6-4).
Para cada uma das três novas regras, a função de conversão associada, indicada na secção
seguinte, resulta da combinação das funções de conversão das regras que lhes deram origem.
As restantes regras do sistema prático não partilhadas com o sistema natural.
10.3.2 Regras básicas do sistema prático
Nesta secção, apresentamos as regras do sistema prático.
O sistema prático é um sistema de prova que axiomatiza uma relação de coerção sobre os tipos
da espécie ∗, em L10. As regras do sistema natural estão definidas sobre juízos de coerção da
forma descrita na secção 10.1.2. Cada regra do sistema tem uma função de conversão associada, definida no mesmo contexto Γ do juízo que ocorre na conclusão da regra.
O sistema prático inclui apenas duas regras terminais (cf. definição 10.2.5-1): [Coerção ≤],
[Coerção extra].
Eis a lista integral das regras básicas que definem o sistema prático:
[Coerção ≤]
Γ υ≤τ Γ τ:∗
Γ υ≤cτ
(Γ υ≤cτ)
[Coerção extra]
–
–
–
ϑ[X]≤ extraΩ[X] Γ σ:∗
–
–
Γ ϑ[σ]≤ cΩ[σ]
–
–
(Γ ϑ[σ]≤ cΩ[σ])
[Coerção extra_pro]
X≤ extraΩ[X] Γ υ≤cτ
Γ υ≤cΩ[τ]
(Γ υ≤cΩ[τ])
=ˆ λx:υ. (τ≤ extraΩ[τ]) ((Γ υ≤cτ)x)
[Coerção extra_despro]
ϑ[X]≤ extraX Γ υ≤cτ
Γ ϑ[υ]≤cτ
(Γ ϑ[υ]≤cτ)
=ˆ λx:ϑ[υ]. (Γ υ≤cτ) ((ϑ[υ]≤extraυ) x)
[Coerção name]
Γ υ≤cτ
Γ υ≤cUnit→τ
(Γ υ≤cUnit→τ) exp
[Coerção →]
Γ υ′≤ cυ Γ τ≤ cτ′
Γ υ→τ≤ cυ′→τ′
(Γ υ→τ≤ cυ′→τ′) =ˆ
λf:υ→τ. λx:υ′. (Γ τ≤ cτ′) (f ((Γ υ′≤ cυ)x))
[Coerção {…}]
Γ τ1 ≤cτ1 ′ … Γ τk ≤cτk ′
Γ {l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }≤c{l 1 :τ1 ′‚…‚l k :τk ′}
=ˆ λx:υ. (λy:τ.y) x
=ˆ (ϑ[σ–]≤ extraΩ[σ–])
=ˆ λz:Unit. (Γ υ≤cτ) exp
192
OM – Uma linguagem de programação multiparadigma
(Γ {l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }≤c{l 1 :τ1 ′‚…‚l k :τk ′}) =ˆ
λr:{l 1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }.
{l 1 =(Γ τ1 ≤cτ1 ′) rl1 ‚…‚lk =(Γ τk ≤cτk ′) r.lk }
10.3.3 Consequências da eliminação da regra da
transitividade
As quatro regras, [Coerção ≤] , [Coerção name], [Coerção →] e [Coerção {…}], não são prejudicadas
pela eliminação da regra da transitividade, na medida em que o subsistema por elas constituído
é transitivo por natureza (cf. teorema 10.3.6-8). Assim, em provas de juízos de coerção, estas
quatro regras podem continuar a combinar-se entre si, como no caso do sistema natural.
Já as regras extra são seriamente afectadas pela eliminação da regra da transitividade. Concretamente, nas provas de juízos de coerção, elas perdem a capacidade de se combinarem com
outras regras, sejam elas regras extra ou regras básicas. Por exemplo, da introdução das regras
extra Bool≤extraFloat e Int≤extraFloat não se pode concluir de que o juízo Γ Bool≤ cFloat seja válido
(cf. demonstração do teorema 10.2.7.3.2-3).
Foi para minorar este problema que as três novas regras básicas, específicas do sistema prático, foram criadas. Elas introduzem formas limitadas de transitividade, a que podem aceder
regras extra que tenham determinadas formas preestabelecidas. Concretamente, as regras extra
com a forma X≤extraΩ[X], prevista em [Coerção extra_pro], têm a virtualidade de serem transitivas
à esquerda, e as regras extra da forma ϑ[X]≤extraX, prevista em [Coerção extra_despro], têm a vantagem de serem transitivas à direita (cf. teorema 10.3.6-4). Estas novas regras cobrem pragmaticamente os casos particulares de transitividade que mais nos interessam, com destaque para
as coerções de modo.
Para exemplificar, eis uma longa lista de juízos só são deriváveis no sistema prático (enriquecido as regras extra da biblioteca padrão, cf. secção 10.2.3), em virtude da transitividade
inerente às novas regras do sistema prático:
Γ
Γ
Γ
Γ
Γ
Γ
Γ
Γ
Γ
Γ
Γ
Int
ConstT ValueT Int
Ref Int
Ref Int
Int
ConstT LazyT Int
ConstT ValueT LogT Int
Float→Ref Int
Int
Int
Int
≤c
≤c
≤c
≤c
≤c
≤c
≤c
≤c
≤c
≤c
≤c
ConstT ValueT Int
Int
Float
GenT Float
LogT Float
ValueT LazyT Int
ConstT ValueT LogT Float
Ref Int→Float
Unit→Unit→Int
Unit→Float
ConstT (Unit→Float)
Um caso de não-transitividade envolvendo regras extra da biblioteca padrão é apresentado
no teorema 10.3.6-8.
10 Sistema de coerções
193
10.3.4 Procedimentos de prova normalizado e prático
Nesta secção, introduzimos dois procedimentos de prova deterministas que se podem usar no
contexto de qualquer sistema de prova e que eliminam todo o indeterminismo e ambiguidade
potenciais desse sistema. Trata-se do procedimento de prova normalizado e do procedimento
de prova prático. O primeiro destes procedimentos será usado na construção de árvores de
prova normalizadas, apenas no contexto da demonstração do teorema 10.3.5-5; o segundo procedimento será adoptado como procedimento de prova oficial do sistema prático.
Começamos por apresentar o procedimento de prova normalizado. Trata-se dum procedimento determinista, muito simples, cuja definição tira partido da forma como as regras do sistema estão ordenadas. É possível tentar usar este procedimento em qualquer sistema de prova,
embora com grau de sucesso variável do ponto de vista da completitude (cf. teoremas 10.3.5-1
e 10.3.5-4).
Definição 10.3.4-1 (Procedimento de prova normalizado) É o procedimento de
prova determinista que explora as regras que compõem o sistema pela ordem em que ocorrem
no sistema, e que escolhe para aplicação efectiva a primeira regra que sucede (se alguma suceder). A árvore de prova é construída, como habitualmente, de forma recursiva, da raiz para as
folhas. (Esta é a conhecida estratégia de pesquisa depth-first com retrocesso, usada na linguagem Prolog).
O procedimento de prova prático é um procedimento determinista que, para além de tirar
partido da ordenação das regras do sistema, incorpora ainda o requisito da geração duma árvore de prova com tamanho mínimo. As árvores de provas de tamanho mínimo tem interesse
pois as funções de conversão que lhes estão associadas minimizam o número de passos de
conversão intermédios: o bom funcionamento do mecanismo dos modos depende desta propriedade (cf. secção 10.1.4).
Definição 10.3.4-2 (Procedimento de prova prático) É o procedimento de prova determinista que explora as regras que compõem o sistema pela ordem em que ocorrem no sistema, e que escolhe para aplicação efectiva, de entre as regras que sucedem (cf. definição
10.2.6-2) e produzem árvores de tamanho mínimo (cf. definição 10.2.5-4), a regra de menor
ordem (i.e. a regra mais acima na lista de regras do sistema). A árvore de prova é construída,
como habitualmente, de forma recursiva, da raiz para as folhas.
O procedimento de prova prático envolve uma questão de minimização. Felizmente, esta
questão pode ser tratada usando a técnica da programação dinâmica, o que garante a possibilidade de implementar este procedimento de forma eficiente.
A utilização da ordenação das regras pelos dois procedimentos de prova serve, antes de
mais nada, para garantir o seu determinismo. Mas também proporciona um útil um mecanismo
de atribuição de prioridades às regras dum sistema de prova: por exemplo, nos sistemas natural
194
OM – Uma linguagem de programação multiparadigma
e prático, a regra [Coerção extra] foi deliberadamente colocada na segunda posição com o objectivo de maximizar a aplicabilidade das regras extra, em detrimento das regras básicas (exceptuando a regra [Coerção ≤] ).
10.3.5 Propriedades dos procedimentos de prova
Nesta secção apresentamos e demonstramos diversas propriedades importantes dos procedimentos de prova normalizado e prático. Mostramos que ambos constituem algoritmos de prova
completos no contexto do sistema prático (cf. teoremas 10.3.5-1 e 10.3.5-3), mas que o procedimento normalizado nem sempre gera árvores de prova com tamanho mínimo (cf. teorema
10.3.5-2). Provamos ainda que nenhum destes procedimentos é completo no contexto do sistema natural.
Começamos por provar que, no contexto do sistema prático, o procedimento de prova normalizado constitui um algoritmo de prova completo.
Teorema 10.3.5-1 No contexto do sistema prático, o procedimento de prova normalizado é
um algoritmo de prova completo.
Prova: Todas as regras básicas não-terminais do sistema prático satisfazem a propriedade da
subfórmula. Assim, de acordo com o teorema 10.2.7.3.2-2, o procedimento de prova normalizado constitui um algoritmo de prova no contexto do sistema prático.
Falta provar que o procedimento de prova normalizado é completo. O facto de ser completo
é afinal uma consequência directa do dois seguintes factos: este procedimento é um algoritmo
e ele usa todas as regras do sistema. Vejamos o detalhe da demonstração.
Seja ≤ c a relação binária entre tipos definida pelo sistema prático. Seja ≤ n a relação binária
entre tipos determinada pelo procedimento de prova normalizado ao ser usado no sistema prático. Vamos provar que Γ ≤c⊂≤ n, ou seja:
• Se for possível provar Γ υ≤cτ usando uma árvore de prova ψc, então também é possível
provar Γ υ≤nτ através duma árvore de prova ψn. (Na demonstração, deixaremos o contexto Γ implícito).
A demonstração efectua-se por indução na altura da árvore de prova ψc.
Caso base: Se altura(ψc)=1, então a prova ψc de υ≤ cτ foi efectuada através duma única aplicação
duma das suas regras terminais: [Coerção ≤] ou [Coerção extra]. Vamos mostrar que nestes casos
também é possível provar υ≤ nτ.
• Caso [Coerção ≤]: Neste caso temos υ≤τ. Aplicando o procedimento de prova normalizado a υ≤nτ e, portanto, tentando sequencialmente as várias regras que definem ≤n, verificamos que a primeira regra tentada, a regra [Coerção ≤], sucede imediatamente.
10 Sistema de coerções
195
• Caso [Coerção extra]: Aplicando o procedimento de prova normalizado a υ≤nτ, tentamos
primeiro a regra [Coerção ≤] . Se suceder a prova está obtida. Caso contrário a regra tentada a seguir, [Coerção extra], sucede de certeza.
Caso geral: Assumimos, como hipótese de indução, que a tese é válida para todas as provas ψc
com altura altura(ψc)=m. Vamos provar que a tese também é válida para todas as provas ψ′c tais
que altura(ψ′ c)=m+1. Faremos uma análise de casos baseada na última regra aplicada na prova
ψ′c.
• Casos [Coerção ≤], [Coerção extra_pro]: casos impossíveis porque neste caso teríamos
altura(ψ′c)=1, o que não pode ser pois estamos a assumir que altura(ψ′c)=m+1>1.
• Caso [Coerção extra_despro]: Como esta foi a última regra aplicada, estamos perante uma
asserção da forma ϑ[υ]≤ cτ, com prova ψ′ c, deduzida a partir de υ≤cτ, a qual tem uma
prova com altura inferior a m+1 (nas condições da hipótese de indução). Apliquemos
então o procedimento de prova normalizado a ϑ[υ]≤ nτ. Percorrendo sequencialmente as
regras que definem ≤n, tentamos sucessivamente as regras [Coerção ≤] e [Coerção extra]. Se
alguma delas suceder a prova está obtida. Se nenhuma delas suceder, e como o procedimento não entrou em ciclo, por ser um algoritmo, tenta-se a regra [Coerção extra_despro] ,
a qual sucede de certeza.
Provemos que, efectivamente, a regra [Coerção extra_despro] sucede nesta situação. A prova de υ≤cτ está nas condições da hipótese de indução pelo que existe uma prova de
υ≤nτ. Aplicando então a regra [Coerção extra_despro] a esta asserção obtém-se imediatamente uma prova ψ′ n para ϑ[υ]≤ nτ.
• Casos [Coerção extra_despro], [Coerção name] : Estes casos provam-se exactamente como o
caso anterior.
• Caso [Coerção →]: Como esta foi a última regra aplicada, estamos perante uma asserção
da forma υ→τ≤cυ′→τ′, com prova ψ′c, deduzida a partir de υ′≤ cυ e de τ≤cτ′, as quais têm
provas com alturas inferiores a m+1 (nas condições da hipótese de indução). Apliquemos então o procedimento de prova normalizado a υ→τ≤ nυ′→τ′. Percorrendo sequencialmente as várias regras que definem ≤n, tentam-se sucessivamente várias regras. Se
alguma das regras tentadas antes de [Coerção →] suceder, a prova está obtida. Se
nenhuma delas suceder, e como o procedimento não entrou em ciclo, tenta-se a regra
[Coerção →], a qual sucede de certeza:
Efectivamente, as provas de υ′≤cυ, τ≤cτ′ estão nas condições da hipótese de indução
pelo que existem provas de υ′≤nυ, τ≤nτ′. Aplicando, então, a regra [Coerção →] a estas
duas asserções obtém-se imediatamente uma prova ψ′n para υ→τ≤nυ′→τ′.
• Caso [Coerção {…}]: Se esta foi a última regra aplicada é porque estamos perante uma
asserção da forma {l1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }≤c{l 1 :τ1 ′‚…‚l k :τk ′}. Este caso demonstra-se como
os anteriores.
196
OM – Uma linguagem de programação multiparadigma
Ao contrário do procedimento de prova prático, o procedimento de prova normalizado pode
não gerar árvores de prova com tamanho mínimo.
Teorema 10.3.5-2 O procedimento de prova normalizado nem sempre gera uma árvore de
prova com tamanho mínimo.
Prova: Basta apresentar um exemplo. Consideremos o sistema prático de base, enriquecido
com as seguintes 4 regras extra: ConstT T≤extraT, T≤ extraLazyT T, LazyT T≤ extraT, T≤ extraValueT T .
Consideremos a asserção ConstT LazyT Nat≤cValueT LazyT Nat .
Para esta asserção, o procedimento de prova normalizado gera a seguinte árvore de prova,
com tamanho 5:
Γ Nat≤Nat
Γ Nat≤ cNat
ConstT T≤ extraT
Γ LazyT Nat≤ cNat
T≤ extraLazyT T
Γ ConstT LazyT Nat≤cNat
T≤ extraValueT T
Γ ConstT LazyT Nat≤cLazyT Nat
Γ ConstT LazyT Nat≤cValueT LazyT Nat
LazyT T≤extraT
No entanto, sem usar o procedimento de prova normalizado, é possível construir esta outra
árvore de prova, com tamanho 3, apenas:
Γ LazyT Nat≤LazyT Nat
Γ LazyT Nat≤ cLazyT Nat
T≤ extraValueT T
Γ ConstT LazyT Nat≤cLazyT Nat
Γ ConstT LazyT Nat≤cValueT LazyT Nat
ConstT T≤ extraT
Incidentalmente, esta segunda árvore é a que resulta da aplicação do procedimento de prova
prático.
No contexto do sistema prático, o procedimento de prova prático também constitui um algoritmo de prova completo, como vamos mostrar.
Teorema 10.3.5-3 No contexto do sistema prático, o procedimento de prova prático constitui um algoritmo de prova completo.
Prova: Esta prova é parecida com a do teorema 10.3.5-1. No entanto, em alguns pontos a argumentação precisa de ser alterada.
No contexto do sistema prático, o procedimento de prova prático é um algoritmo pois todas
as regras básicas não-terminais do sistema prático satisfazem a propriedade da subfórmula (cf.
teorema 10.2.7.3.2-2).
Para provar que o procedimento de prova prático é completo, representemos por ≤c a relação binária entre tipos definida pelo sistema prático e por ≤p a relação binária entre tipos deter-
10 Sistema de coerções
197
minada pelo procedimento de prova prático ao ser usado no sistema prático. Vamos, portanto,
provar que Γ ≤c⊂≤ p, ou seja:
• Se for possível provar Γ υ≤cτ usando uma árvore de prova ψc, então também é possível
provar Γ υ≤pτ através duma árvore de prova ψp. (Na demonstração, deixaremos o contexto Γ implícito).
A demonstração efectua-se por indução na altura da árvore de prova ψc.
Caso base: Se altura(ψc)=1, então a prova ψc de υ≤ cτ foi efectuada através duma única aplicação
duma das suas regras terminais: [Coerção ≤] ou [Coerção extra] . Vamos mostrar que em cada um
destes casos também é possível provar υ≤pτ.
• Caso [Coerção ≤]: Neste caso temos υ≤τ. Aplicando o procedimento de prova prático a
υ≤pτ e, portanto, explorando sequencialmente as várias regras que definem ≤ p, descobrimos que a primeira regra tentada [Coerção ≤] sucede imediatamente. Obtemos assim
uma árvore de prova ψc≡ψp para υ≤cτ. Já não vale a pena tentar as regras seguintes pois
esta é já a árvore de prova pretendida: tem tamanho 1 e a regra [Coerção ≤] é a regra de
menor ordem do sistema.
• Caso [Coerção extra] : Aplicando o procedimento de prova prático a υ≤ pτ, tentamos primeiro a regra [Coerção ≤]. Se suceder, a árvore de prova pretendida está obtida: é uma
árvore de tamanho 1 e a regra [Coerção ≤] é a de menor ordem no sistema. Caso contrário a regra tentada a seguir, [Coerção extra], sucede de certeza e produz uma árvore de tamanho 1 usando a segunda regra do sistema.
Caso geral: Assumimos, como hipótese de indução, que a tese é válida para todas as provas ψc
com altura altura(ψc)=m. Vamos provar que a tese também é válida para todas as provas ψ′c tais
que altura(ψ′ c)=m+1. Faremos uma análise de casos baseada na última regra aplicada na prova
ψ′c. Apresentamos apenas o caso [Coerção extra_despro], uma vez que, relativamente ao teorema
10.3.5-1, as provas de todos os outros casos seriam alvo de adaptação idêntica:
• Caso [Coerção extra_despro]: Como esta foi a última regra aplicada, estamos perante uma
asserção da forma ϑ[υ]≤ cτ, com prova ψ′ c, deduzido a partir de υ≤cτ, a qual tem uma
prova com altura inferior a m+1 (nas condições da hipótese de indução). Apliquemos
então o procedimento de prova prático a ϑ[υ]≤pτ, tentando sequencialmente todas as regras que definem ≤ p. Pelo menos uma destas regras sucede: a regra [Coerção extra_despro] .
Além disso, as regras que falham não fazem o procedimento entrar em ciclo, pois este é
um algoritmo. Finalmente, sendo não-vazio o conjunto de regras que sucedem, é possível escolher nesse conjunto a regra de menor ordem que produz uma árvore de tamanho mínima.
Provemos que, efectivamente, a regra [Coerção extra_despro] sucede nesta situação. A prova de υ≤cτ está nas condições da hipótese de indução pelo que existe uma prova de
198
OM – Uma linguagem de programação multiparadigma
υ≤pτ.
Aplicando a regra [Coerção extra_despro] a esta asserção obtém-se imediatamente
uma prova ψ′p (não necessariamente de tamanho mínimo) para ϑ[υ]≤ pτ.
No contexto do sistema natural, nem o procedimento de prova normalizado nem o procedimento de prova prático têm poder suficiente para provar todos os juízos de coerções válidos do
sistema. A reordenação das regras do sistema também não é suficiente para tornar estes procedimentos completos. O procedimento geral de prova (cf. definição 10.2.7.3.1-1) já teria o poder de prova necessário, mas não tem interesse prático por não ser um algoritmo.
Teorema 10.3.5-4 No contexto do sistema natural, nem o procedimento de prova normalizado nem o procedimento de prova prático são completos.
Prova: Vamos assumir que foram introduzidas no sistema as duas seguintes regras extra, apenas: Float→(Float→Bool)≤ extra{} e {}≤ extraBool→(Bool→Float). Usando estas duas regras, e ainda a
regra da transitividade, pode provar-se a asserção Float→(Float→Bool)≤ cBool→(Bool→Float) da seguinte forma:
Float→(Float→Bool)≤extra{} {}≤extraFloat→(Float→Bool)
Γ Float→(Float→Bool)≤c{} Γ {}≤cBool→(Bool→Float)
Γ Float→(Float→Bool)≤cBool→(Bool→Float)
No entanto, perante a asserção Float→(Float→Bool)≤cBool→(Bool→Float), tanto o procedimento
de prova normalizado como o procedimento de prova prático tentam usar a regra [Coerção →]
antes de tentar usar a regra [Coerção trans]. A aplicação inicial de [Coerção →] leva à tentativa de
lidar com a subprova de Γ Float≤cBool, a qual, sendo inválida, faz os procedimentos entrar em
ciclo. De facto, não existe árvore de prova para Γ Float≤cBool mas, apesar de tudo, a regra
[Coerção trans] é sempre aplicável e faz crescer a árvore de prova de forma ilimitada.
Podemos tentar reordenar as regras do sistema natural por forma que a regra [Coerção trans]
surja antes da regra [Coerção →]. Mas agora, no novo sistema, é também fácil encontrar asserções válidas que fazem os procedimentos entrar em ciclo. Por exemplo, tomando a regra extra
Int≤ extraFloat , a asserção válida Float→Int≤ cInt→Float faz os procedimentos entrar em ciclo, pois a
regra da transitividade [Coerção trans] é sempre tentada antes da regra [Coerção →] .
10.3.6 Propriedades do sistema prático
Nesta secção provamos diversas propriedades do sistema prático. Eis uma lista comentada dessas propriedades:
• Decidibilidade (cf. teorema 10.3.6-1): Alcançar esta propriedade foi a razão de ser da
introdução do sistema prático.
• Indeterminismo e ambiguidade (cf. teorema 10.3.6-1): A eliminação destas propriedades indesejáveis efectua-se recorrendo ao procedimento de prova prático (cf. regras
10.3.6-2 e 10.3.6-3).
10 Sistema de coerções
199
• Menor generalidade do sistema prático face ao sistema natural (cf. teorema 10.3.6-4):
Esta propriedade resulta do facto do sistema prático ser decidível e do sistema natural
não o ser.
• Coerência e transitividade do subsistema do sistema prático na ausência de regras extra
(cf. teoremas 10.3.6-5 e 10.3.6-7): Esta propriedade mostra que o sistema prático inclui
um núcleo de regras com excelentes propriedades.
• Ambiguidade e não-transitividade da versão do sistema prático caracterizada pelas regras extra da biblioteca padrão (cf. teoremas 10.3.6-6 e 10.3.6-8): Os casos de ambiguidade que surgem por culpa das regras extra que foram incluídas na biblioteca padrão
são raros e estão, à partida, resolvidos pela regra 10.3.6-3; embora se possam construir
exemplos de não transitividade do sistema prático estendido com as regras extra da biblioteca padrão (cf. 10.3.6-8), os casos de transitividade mais importantes são suportados pelas novas três regras básicas, introduzidas no sistema prático (cf. secção 10.3.1).
O primeiro teorema desta secção não diz nada de muito novo. Ele justifica-se por razões de
documentação:
Teorema 10.3.6-1 O sistema prático é decidível, mas também indeterminista e ambíguo.
Prova: Pelo teorema 10.3.5-1 existe um algoritmo de prova completo para o sistema prático.
Portanto, a relação de coerção definida pelo sistema prático é decidível. Já vimos dois algoritmos de prova completos para o sistema prático: o procedimento de prova normalizado (cf.
teorema 10.3.5-1) e o procedimento de prova prático (cf. teorema 10.3.5-3)
O sistema prático é indeterminista: as fontes de indeterminismo que são referidas nos dois
últimos parágrafos da secção 10.2.7.1 mantêm-se no sistema prático.
O sistema prático é ambíguo: o exemplo de ambiguidade apresentado na secção 10.2.7.2
aplica-se também ao caso do sistema prático.
O indeterminismo do sistema prático faz com que as suas regras não definam automaticamente um procedimento de prova determinista. No entanto, já sabemos que o procedimento de
prova prático constitui um algoritmo de prova completo no contexto do sistema prático. Por
essa razão, não perdemos poder de prova se adoptarmos este algoritmo como procedimento de
prova oficial do sistema prático. Assim, introduzimos a seguinte regra:
Regra 10.3.6-2 (Eliminação do indeterminismo) No sistema prático, adoptamos, como procedimento de prova oficial, o procedimento de prova prático. No contexto do sistema
prático, a partir de agora, só serão consideradas árvores de prova geradas por este procedimento.
A regra anterior, ao resolver o problema do indeterminismo, tem o efeito indirecto de também resolver o problema da ambiguidade. Eis o enunciado independente duma regra de resolução de ambiguidade:
200
OM – Uma linguagem de programação multiparadigma
Regra 10.3.6-3 (Eliminação da ambiguidade) No sistema prático, a função de conversão que se associa a cada juízo de coerção é aquela que corresponde à árvore de prova gerada pelo procedimento de prova prático.
O sistema prático foi criado para resolver o problema da indecidibilidade do sistema natural. Vamos confirmar que todas as asserções prováveis no sistema prático são asserções válidas do sistema natural.
Teorema 10.3.6-4 (sistema prático ⊂ sistema natural) A relação de coerção definida pelo sistema prático está estritamente contida na relação de coerção definida pelo sistema
natural.
Prova:
1ªparte: Para mostrar a inclusão da primeira relação na segunda, basta verificar que as três regras novas, introduzidas no sistema prático, podem ser demonstradas usando as regras originais do sistema natural.
Vejamos uma demonstração da nova regra [Coerção extra_pro] usando apenas regras do sistema natural. Note como, partindo das premissas da nova regra se chega à conclusão dessa mesma regra, usando as regras [Coerção extra] e [Coerção trans]:
X≤ extraΩ[X] Γ τ
Γ υ≤cτ
Γ τ≤ cΩ[τ]
Γ υ≤cΩ[τ]
Eis, agora, uma demonstração da nova regra [Coerção extra_despro] usando apenas as regras
do sistema natural [Coerção extra] e [Coerção trans]:
ϑ[X]≤ extraX Γ υ:∗
Γ υ≤cτ
Γ ϑ[υ]≤cυ
Γ ϑ[υ]≤cτ
Finalmente, eis uma demonstração da nova regra [Coerção name] usando só as regras do sistema natural [Coerção nname] e [Coerção trans]:
Γ τ:∗
Γ τ≤ cUnit→τ
Γ υ≤cUnit→τ
Γ υ≤cτ
2ªparte: Para mostrar a não equivalência dos sistemas, basta considerar o teorema 10.2.7.3.2-3.
Note que o sistema prático é gerado a partir do sistema natural, usando a manobra descrita no
enunciado daquele teorema.
O seguinte importante teorema mostra que se fosse eliminada a extensibilidade do sistema
prático (ou do sistema natural), o sistema resultante seria coerente, mesmo permanecendo
indeterminista.
10 Sistema de coerções
201
Teorema 10.3.6-5 (Coerência no sistema
≤/name/→/{…})
O sistema ≤/name/→/{…},
mesmo sendo indeterminista, é coerente.
Prova: O sistema ≤/name/→/{…} é constituído pelas quatro regras: [Coerção ≤] , [Coerção name],
[Coerção →] e [Coerção {…}].
Vamos mostrar que, neste sistema, a função de conversão associada a um juízo de coerção
não depende da árvore de prova usada para provar esse juízo. Como árvore de referência para
o estabelecimento de comparação usaremos a árvore de prova gerada pelo procedimento de
prova normalizado. (Não usamos, na prova, o procedimento de prova prático pois a demonstração ficaria mais complicada).
Vamos, representar por ≤ t a relação binária definida pelo sistema ≤/name/→/{…}, e por ≤ n a
relação binária determinada pelo procedimento de prova normalizado ao ser usado no sistema
≤/name/→/{…}.
Indeterminismo: Para mostrar que o sistema ≤/name/→/{…} é indeterminista mostramos que o
juízo trivial Γ {}→{}≤ t{}→{} admite duas árvores de prova distintas:
Γ {}→{}≤{}→{}
Γ {}→{}≤t{}→{}
Γ {}≤{} Γ {}≤{}
Γ {}≤t{} Γ {}≤t{}
Γ {}→{}≤t{}→{}
Coerência: Vamos provar a seguinte proposição:
• Seja ψt uma árvore de prova para um juízo Γ υ≤tτ no sistema ≤/name/→/{…}. Então o
procedimento de prova normalizado gera uma árvore de prova ψn para o juízo Γ υ≤nτ
tal que (Γ υ≤tτ)=(Γ υ≤nτ) . (Na demonstração, deixaremos o contexto Γ implícito).
Note que, por acidente, esta proposição também mostra a completitude do procedimento de
prova normalizado no sistema ≤/name/→/{…}.
A demonstração é por indução na altura da árvore de prova ψt.
Caso base: Se altura(ψt)=1 então a prova ψt de υ≤ tτ consiste numa única aplicação de [Coerção ≤]:
• Caso [Coerção ≤] : Neste caso υ≤τ . No procedimento normalizado de prova a primeira
regra a ser tentada é também [Coerção ≤], portanto com sucesso garantido. Obtemos
assim uma prova normalizada ψn para υ≤ nτ, com ψn=ψt, donde (Γ υ≤tτ)=(Γ υ≤nτ) , como
pretendíamos.
Caso geral: Assumindo, como hipótese de indução, que o teorema é válido para todas as provas ψt com altura altura(ψt)=m, vamos provar que também é válido para todas as provas ψ′t tais
que altura(ψ′ t)=m+1. Faremos uma análise de casos baseada na última regra aplicada na prova
ψ′t. Os casos a estudar são três: [Coerção name], [Coerção →] e [Coerção {…}].
• Caso [Coerção name]: Se esta foi a última regra aplicada, é porque estamos perante uma
asserção da forma τ≤ tUnit→τ′, com prova ψ′t, deduzida a partir da asserção τ≤ tτ′, a qual
202
OM – Uma linguagem de programação multiparadigma
tem uma prova com altura inferior a m+1 (nas condições da hipótese de indução). Apliquemos então o procedimento normalizado de prova a τ≤nUnit→τ′. Percorrendo sequencialmente as várias regras que definem ≤n, verifiquemos quais são as que podem ser
aplicadas neste caso:
•Tentativa [Coerção ≤] : Caso impossível. Se esta regra sucedesse, ficaríamos a saber que
τ≡Unit→υ≤Unit→τ′ com υ≤τ′. Desta forma τ≤ tτ′ teria a forma Unit→υ≤tτ′. Mas não é possível ter ao mesmo tempo υ≤τ′ e Unit→υ≤ tτ′. (Ideia da prova: Imaginando que υ se inicia
por k≥0 ocorrências de “Unit→” temos de ter υ≡[Unit→] kα e τ′≡[Unit→] kβ, onde α e β não
têm qualquer “Unit→” inicial. Isso significa que a regra Unit→υ≤ tτ′ toma a forma
[Unit→]k+1 υ≤t [Unit →] kβ. Mas, agora, demonstra-se facilmente, por indução em k, que
esta asserção é inválida para qualquer inteiro k).
•Tentativa [Coerção name]: A regra anterior falhou, mas esta regra sucede com toda a
certeza. Efectivamente, a prova de τ≤ tτ′ está nas condições da hipótese de indução pelo
que existe uma prova de τ≤nτ′ com altura inferior a m+1. Aplicando, então, a regra
[Coerção name] a esta asserção obtém-se imediatamente uma prova ψ′n para τ≤ nUnit→τ′.
Outra consequência da aplicação da hipótese de indução é o facto de (τ≤ tτ′)=(τ≤nτ′).
Daqui se deduz imediatamente:
(τ≤tUnit→τ′) exp
= λz:Unit. (τ≤ tτ′) exp
= λz:Unit. (τ≤ nτ′) exp
= (τ≤nUnit→τ′) exp
por [Coerção name]
por [Coerção name]
• Caso [Coerção →] : Se esta foi a última regra aplicada, é porque estamos perante uma
asserção da forma υ→τ≤tυ′→τ′, com prova ψ′t, deduzida a partir das asserções υ′≤ tυ e
τ≤ tτ′, as quais têm provas com alturas inferiores a m+1 (condições da hipótese de indução). Apliquemos então o procedimento normalizado de prova a υ→τ≤nυ′→τ′. Percorrendo sequencialmente as várias regras que definem ≤ n, verifiquemos quais são as que
podem ser aplicadas neste caso:
•Tentativa [Coerção ≤]: Se esta regra suceder ficamos com uma prova ψ′n para
υ→τ≤ nυ′→τ′ com altura(ψ′ n)=1. Ficamos também a saber que υ→τ≤υ′→τ′ e como esta
asserção resultou da aplicação de [Coerção ≤] temos υ′≤υ e τ≤τ′.
Vamos provar que (υ→τ≤tυ′→τ′) = (υ→τ≤ nυ′→τ′):
(υ→τ≤ tυ′→τ′)
= λf:υ→τ. λx:υ′. (τ≤tτ′) (f ((υ′≤tυ)x))
= λf:υ→τ. λx:υ′. (τ≤nτ′) (f ((υ′≤nυ)x))
= λf:υ→τ. λx:υ′. (τ≤τ′) (f ((υ′≤υ)x))
= λf:υ→τ. λx:υ′. (λy:τ′.y) (f ((λy:υ.y)x))
= λf:υ→τ. (λg:υ′→τ′.g) f
por [Coerção →]
por hipótese de indução
*
por definição de (τ≤τ′), (υ′≤υ)
(* na prova de τ≤ nτ′ tenta-se primeiro usar a regra [Coerção ≤], com sucesso imediato, pois sabemos que τ≤τ′; o mesmo para υ≤ nυ′).
10 Sistema de coerções
(υ→τ≤nυ′→τ′)
= (υ→τ≤υ′→τ′)
= λf:υ→τ. (λg:υ′→τ′.g) f
203
por [Coerção ≤]
•Tentativa [Coerção name]: Se a tentativa anterior falhou, pode acontecer que esta regra
suceda e nesse caso ficamos com uma prova ψ′n para υ→τ≤ nυ′→τ′. Ficamos também a
saber que a forma de υ′→τ′ é υ′→τ′≡Unit→υ′′ com υ→τ≤nυ′′. Portanto υ→τ≤tUnit→υ′′ e
υ→τ≤ nυ′′ . Como a primeira destas duas asserções resultou da aplicação de [Coerção →]
concluímos que Unit≤ tυ, τ≤ tυ′′ (esta asserção está nas condições da hipótese de indução),
υ→τ≤ nυ′′ . Como tem de ser υ≡Unit, e usando a hipótese de indução, ficamos com υ≡Unit,
τ≤ nυ′′ e Unit→τ≤ nυ′′ .
Finalmente, poderemos ainda dizer que υ′′≡[Unit→]kυ′′′ com τ≤nυ′′′, onde [Unit→]k representa k>0 ocorrências de “ Unit→”, depois de investigarmos como é que Unit→τ≤ nυ′′ foi
provada. Vamos assumir que Unit→τ≤ nυ′′ resultou de, exactamente, k aplicações terminais sucessivas de [Coerção name] que não podiam ser substituídas por aplicações de
[Coerção ≤], podendo ser k=0 . Neste caso, υ′′≡[Unit→] k→σ′′, sendo Unit→τ≤ n[Unit→]k→σ′′
demonstrado a partir de Unit→τ≤ rσ′′. Indo ainda mais atrás no estudo da demonstração
de Unit→τ≤ nυ′′, façamos agora uma análise de casos sobre a última regra usada na prova
de Unit→τ′≤rσ′′:
•Subcaso [Coerção≤] : Neste caso Unit→τ≤σ′′≡Unit→υ′′′ com τ≤υ′′′.
•Subcaso [Coerção →] : Neste caso Unit→τ≤nυ′′≡Unit→υ′′′ com τ≤ nυ′′′.
•Subcaso [Coerção name]: Subcaso impossível pois [Coerção name] não foi certamente a última regra usada na demonstração de υ′→τ′≤ rσ′′. Recordamos que as k
aplicações terminais de υ′→τ′≤rυ′′ já foram alvo de tratamento.
•Subcaso [Coerção {…}]: Subcaso impossível pois υ′→τ′≤ rσ′′ nunca poderia resultar da aplicação da regra [Coerção {…}].
Finalmente, vamos provar que (υ→τ≤ tυ′→τ′) = (υ→τ≤ nυ′→τ′):
(υ→τ≤ tυ′→τ′) fexp
= (Unit→τ≤tUnit→υ′′) fexp
com fexp ∈ Unit→τ
t
t
= (λf:Unit→τ. λx:Unit. (τ≤ υ′′) (f ((Unit≤ Unit)x)) fexp por [Coerção →]*
= λx:Unit. (τ≤tυ′′) (fexp ((Unit≤tUnit)x))
por [Termo= β λx]
n
= λx:Unit. (τ≤ υ′′) (fexp x)
por hipótese de indução e (Unit≤ tUnit)=id
= λx:Unit. (τ≤n[Unit→]kυ′′′) (fexp x)
porque υ′′≡[Unit→] kυ′′′
k+1
n
= [λx:Unit.]
(τ≤ υ′′′) (fexp x)
por [Coerção name]**
(* aplicamos a regra [Coerção →] pois é essa regra que corresponde à assunção do caso corrente)
(** não há qualquer hipótese de τ≤[Unit→]kυ′′′ para algum k≥1 pois isso violaria o pressuposto da
análise de Unit→τ≤ nυ′′ efectuada atrás. Assim a regra que se tenta a seguir é [Coerção name], e
neste caso sempre com sucesso)
204
OM – Uma linguagem de programação multiparadigma
(υ→τ≤ nυ′→τ′) fexp
= (Unit→τ≤nUnit→υ′′) fexp
= λz:Unit. (Unit→τ≤nυ′′) fexp
= λz:Unit. (Unit→τ≤n[Unit→]kυ′′′) fexp
= [λz:Unit.] k (Unit→τ≤nUnit→υ′′′) fexp
com fexp ∈ Unit→τ
por [Coerção name]*
porque υ′′≡[Unit→] kυ′′′
por [Coerção name]**
se Unit→τ≤Unit→υ′′′
então
= [λz:Unit.] k (Unit→τ≤Unit→υ′′′) fexp
por [Coerção ≤]
k
= [λz:Unit.] (λf:Unit→τ. λx:Unit. (τ≤υ′′′) (f ((Unit≤Unit)x))) fexp
usando Tentativa [Coerção ≤]
k
n
= [λz:Unit.] λx:Unit. (τ≤ υ′′′) (fexp x)
por [Termo= β λx] e (Unit≤ tUnit)=id
senão
= [λz:Unit.] k (λf:Unit→τ. λx:Unit. (τ≤ nυ′′′) (f ((Unit≤nUnit)x))) fexp
por [Coerção →]
k
n
= [λz:Unit.] λx:Unit. (τ≤ υ′′′) (fexp x)
por [Termo= β λx] e (Unit≤ tUnit)=id
(* aplicamos [Coerção name] pois é essa regra que corresponde à assunção da tentativa corrente)
(** não há qualquer hipótese de Unit→τ≤[Unit→]kυ′′′ para algum k≥2 pois isso violaria o pressuposto da análise de Unit→τ≤nυ′′, efectuada atrás. Assim a regra que se tenta a seguir é a regra
[Coerção name], e neste caso sempre com sucesso)
Note que todas estas manipulações são compatíveis com passagem de parâmetros call-by-name já que o argumento fexp é efectivamente uma função de tipo Unit→τ.
•Tentativa [Coerção →] : Mesmo que as regras anteriores tenham falhado, esta regra sucede de certeza. Efectivamente, as provas de υ′≤tυ, τ≤ tτ′ estão nas condições da hipótese
de indução pelo que existem provas de υ′≤nυ, τ≤nτ′ com alturas inferiores a m+1. Aplicando então a regra [Coerção →] a estas duas asserções obtém-se imediatamente uma
prova ψ′ n para υ→τ≤nυ′→τ′. Outra consequência da aplicação da hipótese de indução é o
facto de (υ′≤tυ)=(υ′≤ nυ) e (τ≤ tτ′)=(τ≤nτ′). Daqui se deduz imediatamente:
(υ→τ≤ tυ′→τ′)
= λf:υ→τ. λx:υ′. (τ≤tτ′) (f ((υ′≤tυ)x))
= λf:υ→τ. λx:υ′. (τ≤nτ′) (f ((υ′≤nυ)x))
(τ≤tτ′)=(τ≤nτ′)
= (υ→τ≤ nυ′→τ′)
por [Coerção →]
porque (υ′≤tυ)=(υ′≤ nυ) e
• Caso [Coerção {…}]: Se esta foi a última regra aplicada é porque estamos perante uma
asserção da forma {l1 :τ1 ‚…‚lk :τk ‚…‚ln :τn }≤c{l 1 :τ1 ′‚…‚l k :τk ′}. Este caso demonstra-se como
o anterior. Tentam-se as várias regras que definem a relação ≤ t sequencialmente: a regra [Coerção ≤] pode suceder; as regras [Coerção name] e [Coerção →] falham de certeza;
finalmente, regra [Coerção {…}] sucede sempre. Tanto no caso [Coerção ≤] como no caso
[Coerção {…}] é possível tirar as conclusões que o teorema exige usando a hipótese de
indução.
10 Sistema de coerções
205
Com o teorema anterior ficamos a saber que no sistema prático, a ocorrerem situações de
ambiguidade, elas serão sempre provocadas pelas regras extra. Vamos exibir um caso de ambiguidade que ocorre na biblioteca padrão da linguagem OM. Este exemplo tem apenas interesse
académico, pois já resolvemos o problema da ambiguidade mediante a introdução da regra
10.3.6-3.
Teorema 10.3.6-6 (Ambiguidade da biblioteca padrão) Existe pelo menos um
caso de ambiguidade provocado pelas regras extra da biblioteca padrão da linguagem OM (e
resolvido pela regra 10.3.6-3).
Prova: Vamos mostrar que o juízo de coerção Γ Bool→Bool≤cBool→GenT Bool admite duas árvores de prova, cada uma delas com procedimentos de conversão associados distintos.
Se o juízo for provado usando a regra [Coerção →] de forma directa, a função de conversão
que lhe fica associada é a seguinte:
(Γ Bool→Bool≤cBool→GenT Bool)
=ˆ λf:Bool→Bool. λx:Bool. (Γ Bool≤ cGenT Bool) (f x)
= λf:Bool→Bool. λx:Bool. single (f x)
Se o juízo for provado usando directamente a regra extra X→Bool≤ extraX→GenT X , a função
de conversão que lhe fica associada é a seguinte:
(Γ Bool→Bool≤cBool→GenT Bool)
=ˆ λf:Bool→Bool. λx:Bool. (if f x then single x else fail)
As duas funções de conversão são diferentes. Basta tomar f =ˆ λx:true para ver isso.
O próximo teorema é importante pois mostra que se fosse eliminada a extensibilidade do
sistema prático (ou do sistema natural), a relação binária por ele definida seria transitiva (mesmo não existindo regra da transitividade explícita).
Teorema 10.3.6-7 (Transitividade do sistema ≤/name/→/{…}) A relação definida pelo
sistema ≤/name/→/{…} é transitiva.
Prova: Seja ≤ r a relação definida pelas quatro regras [Coerção ≤], [Coerção name], [Coerção →], e
[Coerção {…}]. Vamos provar a seguinte proposição:
• Para quaisquer tipos τ , τ′, τ′′, se for possível provar Γ τ≤ rτ′ (usando uma árvore de prova
ψ1 ) e também Γ τ′≤rτ′′ (usando uma árvore de prova ψ2 ), então também é possível provar Γ τ≤ rτ′′ (usando uma árvore de prova ψ3 ). (Na demonstração, deixaremos o contexto Γ implícito).
Faremos a demonstração por indução na altura máxima das árvores de prova ψ1 e ψ2 . A demonstração não depende da ordenação das regras, nem de qualquer procedimento de prova específico.
Caso base: Se max(altura(ψ1),altura(ψ2))=1 então a prova ψ1 de τ≤ rτ′ e a prova ψ2 de τ′≤rτ′′ consistem ambas em utilizações directas da regra [Coerção ≤]. Verifica-se portanto que τ≤τ′ e τ′≤τ′′ . Por
206
OM – Uma linguagem de programação multiparadigma
transitividade de ≤ obtém-se τ≤τ′′. Finalmente, aplicando novamente [Coerção ≤] concluímos
τ≤ rτ′′, como pretendíamos.
Caso geral: Assumindo como hipótese de indução a validade do teorema para todos os pares
de provas ψ1 e ψ2 tais que max(altura(ψ1),altura(ψ2))=m, vamos provar a validade do teorema para
todos os pares de provas ψ′ 1 e ψ′ 2 tais que max(altura(ψ′1),altura(ψ′2))=m+1.
Faremos uma análise de casos baseada na última regra aplicada na prova de ψ′1 e na última
regra aplicada na prova de ψ′2. Havendo quatro regras no sistema que define ≤ r, há dezasseis
casos a considerar (a demonstração é longa e não-trivial):
• Caso [Coerção ≤], [Coerção ≤]: Este caso não ocorre pois estamos a assumir que as provas
ψ′1 e ψ′ 2 verificam a condição max(altura(ψ′ 1), altura(ψ′ 2))=m+1>1.
• Caso [Coerção ≤], [Coerção name]: Neste caso temos υ≤υ′ e υ′≤rUnit→υ′′, demonstradas a
partir de υ′≤ rυ′′. Esta última asserção está nas condições da hipótese de indução. Mas,
através da regra [Coerção ≤] é possível provar a asserção auxiliar υ≤ rυ′. Como esta prova
tem altura 1, esta asserção também se encontra nas condições da hipótese de indução.
Aplicando a hipótese de indução a υ≤ rυ′, υ′≤ rυ′′ obtemos υ≤ rυ′′. Finalmente, usando a regra [Coerção name] obtemos υ≤rUnit→υ′′, como pretendíamos.
• Caso [Coerção name], [Coerção ≤]: Vamos considerar que a primeira asserção resultou de k
aplicações terminais da regra [Coerção name], com k≥1. Desta forma a primeira asserção
tem a forma υ≤rUnitk→υ′ e a segunda a forma Unitk→υ′≤Unitk→υ′′ , demonstradas a partir
de υ≤ rυ′ e υ′≤υ′′. Estas duas asserções estão nas condições da hipótese de indução. Façamos agora uma análise de casos sobre a última regra usada na demonstração de υ≤rυ′:
•Subcaso [Coerção ≤]: Neste caso temos υ≤υ′, e imediatamente, por transitividade de ≤,
obtemos υ≤υ′′. Usando a regra [Coerção ≤] obtemos υ≤ rυ′′. Usando a regra [Coerção name] k
vezes obtemos obtemos υ≤rUnitk→υ′′ , como pretendíamos.
•Subcaso [Coerção →]: Neste caso υ≤ rυ′ toma a forma ι→σ≤ rι′→σ′, demonstrada a partir
de ι′≤ rι e σ≤rσ′. Assim υ′≤υ′′ toma a forma ι′→σ′≤ι′′→σ′′ com ι′′≤ι′ e σ′≤σ′′. Usando a regra
[Coerção ≤] obtemos ι′′≤ rι′ e σ′≤rσ′′, provadas com árvores de altura 1. Temos assim que
ι′≤rι, σ≤rσ′, ι′′≤ rι′ e σ′≤ rσ′′ estão nas condições da hipótese de indução. Aplicando esta
hipótese duas vezes obtemos ι′′≤ rι e σ≤ rσ′′. Usando agora a regra [Coerção →] obtemos
ι→σ≤ rι′′→σ′′, ou seja υ≤rυ′′ . Usando a regra [Coerção name] k vezes obtemos υ≤rUnitk→υ′′ ,
como queríamos.
•Subcaso [Coerção {…}]: Neste caso υ≤rυ′ toma a forma {a:α,b:β,c:χ}≤r{a:α′,b:β′}, demonstradas a partir de α≤ rα′ e β≤ rβ′. Assim υ′≤υ′′ toma a forma {a:α′,b:β′}≤{a:α′′} com
α′≤α′′ . Usando a regra [Coerção ≤] obtemos α′≤rα′′, provada usando uma árvore de altura
1. Temos assim que α≤rα′ e α′≤rα′′ estão nas condições da hipótese de indução. Aplicando-a obtemos α≤ rα′′. Usando agora a regra [Coerção {…}] obtemos {a:α,b:β,c:χ}≤ r{a:α′′} ou
10 Sistema de coerções
207
seja υ≤ rυ′′. Usando a regra [Coerção name] k vezes obtemos obtemos υ≤rUnitk→υ′′ , como
pretendíamos.
•Subcaso [Coerção name]: Subcaso impossível pois [Coerção name] não foi certamente a
última regra usada na demonstração de υ≤rυ′. Recordamos que as k aplicações terminais
da primeira asserção já foram alvo de tratamento.
• Caso [Coerção name], [Coerção name]: Vamos considerar que a segunda asserção resultou
de k aplicações terminais da regra [Coerção name], com k≥1. Desta forma a primeira
asserção tem a forma υ≤rUnit→υ′ e a segunda a forma Unit→υ′≤Unitk→υ′′ , demonstradas
a partir de υ≤rυ′ e de Unit→υ′≤rυ′′. Estas duas asserções estão nas condições da hipótese
de indução. Façamos agora uma análise de casos sobre a última regra usada na demonstração de Unit→υ′≤rυ′′:
•Subcaso [Coerção ≤] : Neste caso temos Unit→υ′≤υ′′≡Unit→σ′′ com υ′≤σ′′. Usando agora a
regra [Coerção ≤] prova-se usando uma árvore de altura 1 que υ′≤rσ′′. Esta asserção está
nas condições da hipótese de indução. Aplicando esta hipótese a υ≤rυ′ e υ′≤ rσ′′ obtemos
υ≤rσ′′. Usando agora k+1 vezes [Coerção name] obtemos υ≤ rUnitk→Unit→σ′′, ou seja
υ≤rUnitk→υ′′ , como pretendíamos.
•Subcaso [Coerção →]: Neste caso temos Unit→υ′≤ rυ′′≡Unit→σ′′ com υ′≤rσ′′. Esta asserção
está nas condições da hipótese de indução. Aplicando esta a υ≤rυ′ e υ′≤rσ′′ obtemos
υ≤rσ′′. Usando agora k+1 vezes [Coerção name] obtemos υ≤ rUnitk→Unit→σ′′, ou seja
υ≤rUnitk→υ′′ , como pretendíamos.
•Subcaso [Coerção {…} ]: Caso impossível pois Unit→υ′≤ rυ′′ nunca poderia ter sido
provada usando esta regra.
•Subcaso [Coerção name]: Subcaso impossível pois [Coerção name] não foi certamente a
última regra usada na demonstração de Unit→υ′≤rυ′′. Recordamos que as k aplicações
terminais da segunda asserção já foram alvo de tratamento.
• Caso [Coerção name], [Coerção →]: Neste caso temos υ≤rUnit→υ′, Unit→υ′≤rUnit→υ′′,
demonstrados a partir de υ≤rυ′ e de υ′≤ rυ′′. Estas duas asserções estão nas condições da
hipótese de indução. Aplicando-lhes a hipótese de indução obtemos υ≤rυ′′ e, finalmente, usando a regra [Coerção name] obtemos υ≤rUnit→υ′′, como pretendíamos.
• Caso [Coerção →] , [Coerção name]: Vamos considerar que a segunda asserção resultou de
k aplicações terminais da regra [Coerção name], com k≥1. Desta forma a primeira asserção
tem a forma υ→σ≤rυ′→σ′ e a segunda asserção tem a forma υ′→σ′≤Unitk→υ′′ , demonstradas a partir de υ′≤rυ, σ≤ rσ′, υ′→σ′≤rυ′′. Estas três asserções estão nas condições da
hipótese de indução. Façamos agora uma análise de casos sobre a última regra usada na
demonstração de υ′→σ′≤rυ′′:
•Subcaso [Coerção ≤]: Neste caso temos υ′→σ′≤υ′′≡υ′′′→σ′′′ com υ′′′≤υ′, σ′≤σ′′′. Através da
regra [Coerção ≤] é possível provar as asserções auxiliares υ′′′≤ rυ′ e σ′≤ rσ′′′ usando ár-
208
OM – Uma linguagem de programação multiparadigma
vores de altura 1: estas asserções estão portanto nas condições da hipótese de indução.
Aplicando a hipótese de indução a υ′≤ rυ, σ≤ rσ′, υ′′′≤ rυ′ e σ′≤rσ′′′ obtemos υ′′′≤rυ e σ≤rσ′′′.
Usando [Coerção →] , obtemos υ→σ≤rυ′′′→σ′′′, ou seja υ→σ≤ rυ′′. Finalmente, aplicando k
vezes a regra [Coerção name] a esta última asserção obtemos υ→σ≤rUnitk→υ′′ , como
pretendíamos.
•Subcaso [Coerção →]: Neste caso temos υ′→σ′≤rυ′′≡υ′′′→σ′′′ com υ′′′≤ rυ′, σ′≤ rσ′′′. Através
da regra [Coerção ≤] é possível provar as asserções auxiliares υ′′′≤ rυ′ e σ′≤rσ′′′. Estas
asserções estão nas condições da hipótese de indução. Aplicando a hipótese de indução
a υ′≤ rυ, σ≤rσ′, υ′′′≤rυ′ e σ′≤rσ′′′ obtemos υ′′′≤ rυ e σ≤rσ′′′. Usando [Coerção →], obtemos
υ→σ≤rυ′′′→σ′′′, ou seja υ→σ≤rυ′′ . Finalmente, aplicando k vezes a regra [Coerção name] a
esta última asserção obtemos υ→σ≤ rUnitk→υ′′ , como pretendíamos.
•Subcaso [Coerção {…}]: Subcaso impossível pois υ′→σ′≤rσ′′ nunca poderia resultar da
aplicação da regra [Coerção {…}].
•Subcaso [Coerção name]: Subcaso impossível pois [Coerção name] não foi certamente a
última regra usada na demonstração de υ′→σ′≤rσ′′. Recordamos que as k aplicações terminais da segunda asserção já foram alvo de tratamento.
• Caso [Coerção →] , [Coerção →]: Neste caso temos υ→σ≤ rυ′→σ′ e υ′→σ′≤ rυ′′→σ′′ , demonstrados a partir de υ′≤rυ, σ≤ rσ′, υ′′≤rυ′, σ′≤ rσ′′. Estas quatro asserções estão nas condições
da hipótese de indução. Aplicando duas vezes a hipótese de indução obtemos υ′′≤rυ,
σ≤ rσ′′ donde através da regra [Coerção →] obtemos υ→σ≤rυ′′→σ′′ , como pretendíamos.
• Caso [Coerção ≤], [Coerção →]: Neste caso temos υ→σ≤υ′→σ′ e υ′→σ′≤ rυ′′→σ′′ , demonstrados a partir de υ′≤υ, σ≤σ′ , υ′′≤rυ′, σ′≤rσ′′. Aplicando a regra [Coerção ≤] às duas primeiras asserções obtemos υ′≤ rυ, σ≤rσ′, υ′′≤rυ′, σ′≤ rσ′, as quais estão todas nas condições da
hipótese de indução. Usando esta hipótese obtemos υ′′≤rυ, σ′′≤ rσ. Finalmente, usando
[Coerção →] obtemos υ→σ≤rυ′′→σ′′ , como pretendíamos.
• Caso [Coerção →] , [Coerção ≤]: Neste caso temos υ→σ≤rυ′→σ′ e υ′→σ′≤υ′′→σ′′ , demonstrados a partir de υ′≤ rυ, σ≤ rσ′, υ′′≤υ′, σ′≤σ′′. Aplicando a regra [Coerção ≤] às duas últimas
asserções obtemos υ′≤rυ, σ≤rσ′, υ′′≤ rυ′, σ′≤rσ′, as quais estão todas nas condições da
hipótese de indução. Usando esta hipótese obtemos υ′′≤rυ, σ′′≤ rσ. Finalmente, usando
[Coerção →] obtemos υ→σ≤rυ′′→σ′′ , como pretendíamos
• Caso [Coerção {…}], [Coerção name]: Esta demonstração tem muitas semelhanças com o
caso [Coerção →], [Coerção name]. Sem perder generalidade, vamos assumir que a
primeira asserção tem a forma: {a:α,b:β,c:χ}≤r{a:α′,b:β′}. Vamos também considerar que
a segunda asserção resultou de k aplicações terminais da regra [Coerção name], com k≥1.
Tomos assim {a:α,b:β,c:χ}≤r{a:α′,b:β′}, {a:α′,b:β′}≤rUnitk→υ′′ , demonstradas a partir de
α≤ rα′, β≤rβ′, {a:α′,b:β′}≤ rυ′′ . Estas três asserções estão nas condições da hipótese de
indução. Façamos agora uma análise de casos sobre a última regra usada na demonstração de {a:α′,b:β′}≤ rυ′′:
10 Sistema de coerções
209
•Subcaso [Coerção ≤]: Neste caso temos {a:α′,b:β′}≤υ′′≡{a:α′′} com α′≤α′′ . Através da regra
[Coerção ≤] é possível provar a asserção auxiliar α′≤rα′′ usando árvores de altura 1: esta
asserção está portanto nas condições da hipótese de indução. Aplicando a hipótese de
indução a α≤rα′, α′≤rα′′ obtemos α≤ rα′′. Usando [Coerção {…}], obtemos {a:α,b:β,c:χ}≤ r
{a:α′′}, ou seja {a:α,b:β,c:χ}≤rυ′′. Finalmente, aplicando k vezes a regra [Coerção name] a
esta última asserção obtemos {a:α,b:β,c:χ}≤rUnitk→υ′′ , como pretendíamos.
•Subcaso [Coerção →]: Subcaso impossível pois {a:α′,b:β′}≤ rυ′′ nunca poderia resultar da
aplicação da regra [Coerção →] .
•Subcaso [Coerção {…}]: Neste caso temos {a:α′,b:β′}≤ rυ′′≡{a:α′′} com α′≤rα′′. Estas última
asserção está nas condições da hipótese de indução. Aplicando a hipótese de indução a
α≤ rα′, α′≤rα′′ obtemos α≤ rα′′. Usando [Coerção {…}], obtemos {a:α,b:β,c:χ}≤ r{a:α′′}, ou seja
{a:α,b:β,c:χ}≤ rυ′′ . Finalmente, aplicando k vezes a regra [Coerção name] a esta última
asserção obtemos {a:α,b:β,c:χ}≤ rUnitk→υ′′ , como pretendíamos.
•Subcaso [Coerção name]: Subcaso impossível pois [Coerção name] não foi certamente a
última regra usada na demonstração de {a:α′,b:β′}≤rυ′′. Recordamos que as k aplicações
terminais da segunda asserção já foram alvo de tratamento.
• Caso [Coerção {…}], [Coerção {…}]: Demonstração estruturalmente idêntica à demonstração do caso [Coerção →], [Coerção →] .
• Caso [Coerção name], [Coerção {…}]:
• Caso [Coerção →], [Coerção {…}]:
• Caso [Coerção {…}], [Coerção →]: Casos que não podem ocorrer devido à estrutura das
conclusões das regras.
• Caso [Coerção ≤] , [Coerção {…}]:
• Caso [Coerção {…}], [Coerção ≤]: Demonstrações estruturalmente idênticas às demonstrações dos casos [Coerção ≤], [Coerção →] e [Coerção →], [Coerção ≤].
O teorema anterior mostra que a não-transitividade da relação binária entre tipos definida
pelo sistema prático resulta exclusivamente da possibilidade de introdução de regras extra (e
claro, da inexistência duma regra de transitividade explícita). Além disso, nem todas as regras
extra são não-transitivas: de facto, como já vimos na secção 10.3.3, o sistema prático suporta
uma forma limitada de transitividade, a que têm acesso regras extra com certas formas predeterminadas.
De qualquer forma, fora destes limites conseguem-se encontrar exemplos de não-transitividade, inclusivamente no âmbito restrito na biblioteca padrão da linguagem, como mostramos a
seguir.
Teorema 10.3.6-8 (Não-transitividade da biblioteca padrão) No contexto do sistema prático, as regras extra da biblioteca padrão dão origem a casos de não-transitividade.
210
OM – Uma linguagem de programação multiparadigma
Prova: Para provar o teorema, basta mostrar que existem três tipos τ, τ′, τ′′ tais que τ≤cτ′ e
τ′≤cτ′′ mas não τ≤cτ′′. Vamos assumir que o sistema prático inclui apenas as regras extra da
biblioteca padrão e vamos tirar partido da regra extra X→Bool≤ cX→GenT X .
Tomemos então τ≡Float→Bool , τ′≡Int→Bool, τ′′≡Int→GenT Int. As asserções τ≤ cτ′ e τ′≤cτ′′ são válidas como mostram as seguintes árvores de prova:
Int≤ extraFloat Γ Bool≤Bool
Γ Int≤ cFloat Γ Bool≤ cBool
Γ Float →Bool≤cInt→Bool
X→Bool≤extraX→GenT X Γ Int:∗
Γ Int→Bool≤ cInt→GenT Int
No entanto a asserção τ≤cτ′′ não pode ser deduzida do sistema, sendo portanto inválida.
Imaginando uma prova de τ≤cτ′′ construída da conclusão para as premissas,
…
Γ Float →Bool≤cInt→GenT Int
verificamos que nenhuma das regras pode ser aplicada em último lugar: a regra [Coerção →] não
o permite pois é falso que Γ Bool≤ cGenT Int; a regra [Coerção ≤] também não é aplicável pois é
falso que Γ Float →Bool≤Int→GenT Int ; as regras [Coerção name], [Coerção {…}] e as regras extra não
são aplicáveis pois a estrutura das suas conclusões é incompatível com a estrutura da asserção
que se pretendia provar.
Capítulo 11
Linguagem OM
A linguagem abstracta L10 e o respectivo modelo semântico foram desenvolvidos em paralelo
ao longo dos capítulos precedentes. A linguagem OM, que vamos introduzir no capítulo corrente, concretiza um sistema de programação prático baseado na linguagem abstracta L10.
A linguagem OM integra todas as componentes de L10, nomeadamente:
• Objectos mutáveis com parte privada (introduzidos no capítulo 7). Classes e tipos-objecto (introduzidos nos capítulos 5 e 8). Relação de subtipo (introduzida no capítulo 4).
Mecanismo de herança flexível (introduzido no capítulo 5). Polimorfismo de classe (introduzido no capítulo 6 e na secção 8.3.1). Mecanismo dos modos (introduzido no capítulo 9). Sistema de coerções extensível (introduzido no capítulo 10).
Além disso, a linguagem prática OM introduz diversos elementos suplementares:
• Amplia os mecanismos de especificação da faceta estática dos modos: concretamente,
introduz sobreposição de parte da sintaxe básica (cf. secção 11.3) e componentes globalizadas (cf. secção 11.5);
• Preocupa-se com os seguintes pragmáticos: sintaxe (cf. secção 11.1), regras de nomeação de classes e tipos (cf. secção 11.2), regras de resolução de nomes (cf. secção 11.6),
introdução dum nível privilegiado no qual a linguagem pode ser estendida ou alterada
(cf. secção 11.4);
• Introduz uma biblioteca de classes (cf. secção 11.7).
O presente capítulo é dedicado à apresentação da linguagem concreta OM, incluindo a descrição destes novos elementos. A leitura deste capítulo é indispensável para se compreender a
definição dos modos de biblioteca que se encontram no capítulo que se segue a este.
11.1 Sintaxe da linguagem OM
A sintaxe da linguagem OM é baseada na sintaxe das linguagens C e C++. Não formalizamos
aqui essa sintaxe: apresentaremos apenas alguns exemplos e chamaremos a atenção para certos
pormenores importantes.
Mostramos primeiro como se traduzem para OM diversos tipos e termos que já nos eram
familiares na linguagem L10:
212
OM – Uma linguagem de programação multiparadigma
L10
Ref Bool
Unit
()
λx:Bool.x
id =ˆ λX.λx:X.x
o.m …
self.m …
SELFC#m …
C =ˆ class{…}
C =ˆ class\s{…}
C =ˆ λX≤* I.∀Y≤ * J.class{…}
M =ˆ mode X≤* I. {…}
OM
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
Bool&
() ou void
não tem representação literal
Bool fun(Bool x){ return x ; }
[X] X id(X x) { return x ; }
o.m(…)
self.m(…) ou m(…)
SELFC#m(…) ou #m(…)
class C {…}
class C : s {…}
class C[X<I,Y<J] {…}
mode M[X<I] {…}
Referimos agora diversos aspectos sintácticos importantes, e qual o seu significado.
Na linguagem OM, o operador '=' está definido: a atribuição é expressa usando o operador
':=', e a igualdade usando o operador '=='.
No interior das classes as componentes privadas são declaradas usando a palavra reservada
“priv”. No caso das componentes públicas, o uso da declaração “ pub” é opcional; recordamos
que as variáveis (de instância ou de classe) ditas públicas, são na realidade semipúblicas no
sentido por nós usado na secção 7.3.1.
Os nomes de tipo predefinidos SAMET e SELFT estão disponíveis no interior de qualquer
classe, na qual representam respectivamente o tipo público e o tipo privado gerados por essa
classe (ou por qualquer das suas subclasses, já que estes nomes são reinterpretados nas componentes herdadas, cf. capítulos 5 e 7).
A respeito de SELFT , recordamos que, dentro duma classe, os objectos dessa mesma classe
são criados como objectos de tipo SELFT , ou seja como objectos que não protegem a sua parte
privada. Assim facilita-se a inicialização desses objectos. Além disso não se prejudica a segurança desses objectos pois quando um objecto do tipo SELFT abandona a classe onde foi
criado, o seu tipo muda automaticamente para SAMET (cf. capítulo 7).
Em OM, um programa é uma colecção de classes e modos que, obrigatoriamente, inclui
uma classe chamada Main contendo pelo menos um método de classe público com assinatura
#main:Unit→Unit. Todo o programa é activado usando a expressão Main#main().
11.2 Nomeação das classes e tipos-objecto
Na linguagem OM, existem três categorias de entidades com a capacidade de gerar tipos ou
operadores de tipo. São elas:
• Classes não-paramétricas – geram tipos-objecto e têm a forma:
class <nome_da_classe> : <superclasse> { … }
• Classes paramétricas – geram operadores de tipo e têm a forma:
class <nome_da_classe>[X1<I1, …,Xn<In] : <superclasse> { … }
11 Linguagem OM
213
• Modos – geram operadores de modo (cf. secção 9.1.3) e têm a forma:
mode <nome_do_modo>[X<I] { … }
Como se pode observar nos três esquemas sintácticos acima apresentados, a introdução de
uma classe ou de um modo obriga sempre à atribuição dum nome para essa entidade.
Nas subsecções seguintes, vamos apresentar e discutir as regras de nomeação das entidades
geradoras de tipo (classes e modos) e aos tipos por elas gerados. Essas regras também se
estendem às variáveis de tipo. A ideia base é a seguinte: um nome de tipo representa sempre
também um nome de classe e vice-versa, mesmo no caso em que o nome é uma variável de
tipo.
11.2.1 Regras de nomeação
Na linguagem OM, é a seguinte a regra base de nomeação de entidades geradoras de tipo e
dos respectivos tipos gerados:
- O nome duma classe ou modo é partilhado com o tipo ou operador que eles geram.
Só se podem introduzir novos tipos de forma indirecta, através da definição de novas
classes e novos modos. Por isso, a regra inversa da anterior também é verdadeira:
- Todo o nome de tipo ou de operador é o nome da classe ou modo que os geraram.
A linguagem OM suporta polimorfismo de classe, uma variante de polimorfismo paramétrico que introduzimos na secção 8.3.1. Trata-se duma forma de polimorfismo P =ˆ λX≤* I.e que
permite abstrair simultaneamente uma classe não-paramétrica e o respectivo tipo-objecto gerado: a variável de abstracção X representa um par <classe-não-paramétrica, tipo-objecto>. Assim, introduzimos uma nova regra, relativa a variáveis de tipo:
- Uma variável de tipo é também variável de classe e vice-versa.
Finalmente, introduzimos a seguinte convenção relativa à variável de tipo SAMET:
- A variável de tipo SAMET também representa a classe não-paramétrica SELFC.
Esta última regra destina-se a permitir que uma abstracção paramétrica P =ˆ λX≤* I.e possa ser
instanciada com a variável de tipo SAMET, já que, em OM, os argumentos de instanciação
duma abstracção paramétrica terão sempre de representar um par <classe não-paramétrica, um
tipo-objecto>.
11.2.2 Justificação das regras de nomeação
Todas estas regras de nomeação destinam-se a tornar mais prático o uso da linguagem. Elas
permitem eliminar dos programas todas as declarações de tipo (as quais já estão implícitas na
estrutura das classes e modos) e reduzir a metade o número de argumentos nas abstracções
paramétricas (devido à interpretação dual dos nomes de tipo na linguagem).
214
OM – Uma linguagem de programação multiparadigma
11.2.3 Aspectos práticos
Usa-se o contexto para determinar qual a interpretação duma variável de tipo requerida em
cada caso.
• No contexto X#…, a expressão X representa uma classe não-paramétrica;
• No contexto C[X] e no contexto M X, a expressão X representa um par <tipo-objecto,
classe não-paramétrica>;
• Nos restantes contextos X representa um tipo ou um operador.
As nossas regras de nomeação não comprometem o princípio, que prosseguimos, de separar
os conceitos de classe e de tipo-objecto (cf. secções 4.3.1, 4.3.6, 7.4). Repare no seguinte contraste, onde C representa o nome duma classe c :
• Se c implementar um construtor público #pub_new, então a expressão C#pub_new() produz apenas objectos implementados pela classe c ;
• No entanto, uma variável declarada com o tipo C pode conter qualquer objecto com a
estrutura pública prevista no tipo C, portanto objectos não necessariamente da classe c .
11.2.4 Exemplo
Vejamos um pequeno exemplo de nomeação, baseado no modo de biblioteca log.
Em OM, a expressão “ log Int” tem uma interpretação dual. Ela representa a classe dos objectos lógicos sobre o tipo Int, mas também representa o tipo desses mesmos objectos. (Lembramos que, no capítulo 9 e 10, usámos a notação diferenciada “LogT τ” para representar o tipo
dos objectos lógicos (cf. secção 9.1.2.1); agora, em OM, adoptamos uma notação unificada).
Para perceber o que está em causa, vamos analisar a expressão de tipo genérica M C.
Seja m um modo e ϕm o operador de modo gerado por m. Suponhamos que o nome atribuído ao modo foi M. Nestas condições o nome M fica com uma interpretação dual, representando
tanto o modo m como o operador de modo ϕm.
Seja c uma classe não-paramétrica e τc o tipo-objecto gerado por c . Suponhamos que o nome atribuído à classe foi C. Nestas condições o nome C fica com uma interpretação dual, representando tanto a classe c como o tipo-objecto τc.
Com estes pressupostos, a expressão M C fica também com uma interpretação dual, representando tanto a classe classe não-paramétrica (m <τ c,c>), como o tipo-objecto (ϕm τc).
11.3 Sobreposição da sintaxe de OM
Na linguagem OM, todas as construções estruturantes – i.e. classes, modos, abstracções paramétricas, métodos e funções – têm semântica predefinida: exactamente a semântica das cons-
11 Linguagem OM
215
truções correspondentes da linguagem L10. Na linguagem OM, o novo comando return e as
expressões literais também têm semântica predefinida.
No entanto, as restantes expressões e comandos da linguagem OM não têm semântica predefinida.
Chamamos construções de semântica variável às construções sintácticas de OM que não
têm semântica predefinida. A atribuição de semântica a estas construções efectua-se nas classes primitivas e nos modos da linguagem, da forma que apresentamos a seguir.
11.3.1 Atribuição de semântica às construções de
semântica variável
O mecanismo de atribuição de semântica às construções de semântica variável de OM é simples. As construções de semântica variável são simplesmente vistas como açúcar sintáctico
para a invocação de certos métodos de classe especiais, cujo nome se inicia pelo prefixo
“#$def_”.
Chamaremos a esse métodos, métodos “#$def_*”. Todos os métodos “ #$def_*” são definidos
no nível privilegiado (cf. secção 11.4). Assim, uma classe normal não pode definir directamente métodos “#$def_*”, mas repare que os pode herdar das classes privilegeadas $CoreObject
ou Object, por exemplo.
A tradução implícita das construções de semântica variável para métodos “#$def_*” precede
qualquer análise estática do programa, incluindo a aplicação de coerções e a resolução de nomes. As regras gerais de tradução são as seguintes:
( f:T->R )(exp)
( obj:T ).label
T var = val ;
( var:T& ) := exp: ;
comm1 comm2
(exp:()->T) ;
;
( cond:T ) ? thenP : elseP
if( cond:T ) thenP else elseP
if( cond:T ) thenP
while( cond:()->T ) body
do body while( cond:()->T ) ;
for( ini ; cond ; inc ) body
switch( exp:T ) body
raise (exc:T) ;
try comm with( exc:T ) do
T var ;
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
-->
T#$def_apply(f, exp)
T#$def_select(obj, label)
T#$def_init(var, val)
T#$def_assign(var, exp)
$Void#$def_seq(comm1, comm2)
T#$def_comm(exp)
$Void#$def_nop()
T#$def_cond(cond, thenP, elseP)
T#$def_ifthenelse(cond, thenP, elseP)
T#$def_ifthenelse(cond, thenP, true)
T#$def_while(cond, body)
T#$def_dowhile(body, cond)
{ ini; while(cond) {body inc;} }
T#$def_switch(exp, body)
T#$def_raise(T#exc)
T#$def_trywith(comm, T#exc, do)
T#$def_init_implicit(var)
Convencionalmente, nas expressões com a forma “#$def_select(exp,n)” e “$raw_select(exp,n)” a
subexpressão exp nunca é sujeita a qualquer coerção: a linguagem ficaria muito confusa se tal
fosse permitido.
Note que a tradução de qualquer construção da linguagem OM gera uma expressão que
refere sempre uma classe concreta, na qual se assume que o método “#$def_*” referido está dis-
216
OM – Uma linguagem de programação multiparadigma
ponível. Como essa classe é determinada pelo tipo duma certa expressão que ocorre na construção original, isso significa que a linguagem OM suporta sobreposição da sua sintaxe básica
(overloading), sendo no caso trivial a regra de resolução dessa sobreposição.
O aproveitamento deste mecanismo de sobreposição efectua-se no nível privilegiado e materializa-se nas decisões que se tomam relativamente à definição de métodos “#$def_*” nas
classes primitivas e nos modos.
Existe uma pequena particularidade prevista no esquema de tradução: dentro de métodos
declarados com a palavra reservada especial raw (cf. secção 11.4) a tradução efectua-se directamente para primitivas “$raw_*” (cf. secção 11.3.2) em vez de métodos “#$def_*”.
Note ainda que a determinação e o tratamento das ocorrências de identificadores que referenciam implicitamente o objecto self só pode ser efectuada durante a fase de resolução de
nomes do compilador, ficando adiada para esse momento. Cada ocorrência dum identificador n
que tenha estas características é tratada como uma abreviatura de self.n, sendo traduzida para
#$def_select(self,n) (cf. secção 11.6).
11.3.2 Matéria-prima semântica
A linguagem fornece o seguinte conjunto de primitivas paramétricas como matéria prima para
a definição dos métodos “#$def_*” descritos na secção anterior. O nome das primitivas paramétricas inicia-se sempre pelo prefixo “$raw_” (primitivas “ $raw_*”):
As primitivas paramétricas definem-se por tradução para L10. Eis a lista integral das primitivas paramétricas suportadas pela linguagem OM:
[X,R] R $raw_apply(X->R f, X a)
=ˆ ∀X.∀R.λf:X→R.a:X. (f a)
ˆ ∀X.∀R.λobj:X.λlabel:X→R. (label obj)
[X,R] R $raw_select(X obj, X->R label) =
[X] X $raw_init(X& var, X val)
=ˆ ∀X.λx:Ref X.λy:X. (x := y)
[X] X $raw_assign(X& var, X val)
=ˆ ∀X.λx:Ref X.λy:X. (x := y)
[X,Y] () $raw_seq(()->X x, ()->Y y)
=ˆ ∀X.∀Y.λx:Unit→X.λy:Unit→Y. (x ();y ();())
[X] () $raw_comm(()->X x)
=ˆ ∀X..λx:Unit→X. (x ();())
() $raw_nop()
=ˆ ()
[X] X $raw_deref(X& var)
=ˆ ∀X.λx:Ref X. (deref x)
[X] X $raw_cond(Bool cond, ()->X thenP, ()->X elseP)
=ˆ ∀X.λb:Bool.λx:Unit→X.λy:Unit→X. (if b then x () else y ())
[X,Y] () $raw_ifthenelse(Bool cond, ()->X thenP, ()->Y elseP)
[X] () $raw_while(()->Bool cond, ()->X body)
[X] () $raw_dowhile(()->X body, ()->Bool cond)
[X] () $raw_switch(X exp, X->() body)
[Z] () $raw_raise(Z exc)
[X,Y,Z] () $raw_trywith(()->X comm, Z exc, ()->Y do)
As primitivas cuja codificação não apresentamos podem ser facilmente definidas usando
técnicas apresentadas em [Sto77, Sch86].
11 Linguagem OM
217
11.4 Nível privilegiado e recursos especiais
No contexto dos modos e das classes primitivas, o nível de programação diz-se privilegiado.
No contexto das classes não-primitivas, o nível de programação diz-se normal.
No nível privilegiado estão disponíveis certos recursos especiais que permitem que a linguagem seja estendida. Alguns desses recursos destinam-se, especificamente, a suportar a definição da faceta estática dos modos.
O primeiro recurso especial que referimos é bastante modesto: consiste na possibilidade de
se usarem identificadores começados pelo carácter “$”. Esses identificadores são usados na
nomeação de entidades públicas introduzidas no nível privilegiado, a que se pretenda vedar o
acesso a partir do nível normal. As classes primitivas $CoreObject e $DYNAMIC_TYPE são exemplos de tais entidades.
O segundo recurso especial é também muito simples: convencionalmente, no nível privilegiado, a operação de atribuição pode ser aplicada uma variável de qualquer tipo, mesmo que o
tipo em causa não preveja a operação de atribuição (é o caso do tipo const Int, por exemplo).
Neste caso, a inexistente operação de atribuição é automaticamente substituída pela operação
de inicialização explicita, a qual está disponível em todos os tipos.
O terceiro recurso especial consiste na operação $UNCHECKED_RETYPING, uma operação
de mudança de tipo de expressões, não verificada. Pode ser aplicada a qualquer expressão para
mudar o seu tipo sem mudar o seu valor. O sistema de tipos não valida esta operação, sendo
esta a única operação insegura que a linguagem suporta.
Os restantes recursos especiais do nível privilegiado são acedidos por meio das palavras reservadas especiais: global , coercion, raw, macro, primitive. Vamos indicar quais as suas condições de aplicação e qual o efeito de cada uma delas:
• global – Pode ser aplicada a qualquer componente de classe pública. Faz com que essa
componente seja globalizada, ou seja, colocada no espaço de nomes global para ficar
acessível a partir de todas as classes. Uma componente globalizada continua a poder
ser usada como uma componente de classe normal. Mais detalhes sobre as componentes globalizadas na secção 11.5.
• coercion – Pode ser aplicada a qualquer método de classe público com um argumento e
um resultado. Faz com que esse método, dito de coerção, seja assimilado pelo sistema
de coerções como uma regra de coerção extra (cf. secção 10.2.1). O método continua a
poder ser usado como um método de classe normal.
• raw – Pode ser aplicada a qualquer método de instância ou de classe. Faz com que, no
corpo desse método, a atribuição de significado à sintaxe concreta de OM (cf. secção
11.3) seja efectuada usando apenas primitivas “$raw_*”, e não os habituais métodos
“#$def_*”. Também faz com que todas as invocações desse método sejam efectuadas
218
OM – Uma linguagem de programação multiparadigma
usando a primitiva $raw_apply e não o habitual método #$def_apply. As declarações raw
servem para evitar que a semântica da linguagem entre em ciclo.
Na biblioteca são usadas das definições da maioria dos métodos “#$def_*” (e também
na definição nos métodos $dup, de que o modo value necessita). Ainda, os métodos
$access dos modos (cf. secção 9.1.3) são automaticamente considerados raw para efeitos
de invocação. Não existe a primitiva $raw_default_init(.) pelo que todas as variáveis locais
têm de ser explicitamente inicializadas num método raw.
• macro – Pode ser aplicada a qualquer método de instância ou de classe. Serve para indicar que esse método será herdado sob a forma textual, e recompilado em toda a subclasse que o herde. Dentro dum método sujeito à declaração macro podem ser usadas as
duas seguintes primitivas especiais:
MACROWellTyped(exp) – Em cada recompilação, expande-se em true ou false, consoante
a expressão exp esteja bem ou mal tipificada.
MACROForEveryInstVarPair[X](a, b)(ob1,ob2) com – Expande numa sequência de reescritas
do comando com, sendo efectuada uma reescrita por cada par de variáveis de
instância dos objectos ob1, ob2 da classe corrente. Funciona como uma espécie
de iterador, no qual o par de variáveis a e b percorre os sucessivos pares de variáveis de instância de ob1 e ob2. X denota o tipo das variáveis de instância correntemente consideradas (representadas por a e b).
• primitive – (classe primitiva) Etiqueta todas as classes primitivas da biblioteca. As classes primitivas são suportadas pelo sistema de forma especial, que lhes insere funcionalidade primitiva e predefine para elas literais específicos. Dentro duma classe primitiva
o nível de programação é privilegiado. As classes primitivas são: $CoreObject,
$ZeroObject , $AsgnObject, Object , Nil, $DYNAMIC_TYPE, Equality, Int, Float, Bool, Char, Str ,
Array[T], Fun[T,R], Void, Ref[V], Exception . Este é conjunto fixo e predefinido de classes.
• primitive – (método primitivo) Dentro das classes primitivas, etiqueta obrigatoriamente
todos os métodos primitivos que aí sejam introduzidos. Um método primitivo escreve-se sem corpo, pois é o sistema que se encarrega de instalar o código destes métodos.
Este é conjunto fixo e predefinido de métodos.
As três palavras reservadas global, coercion e raw podem ser usadas de forma conjugada, em
todas as combinações possíveis. As palavras reservadas macro e primitive são sempre usadas
isoladamente.
11.5 Componentes globalizadas
No nível privilegiado é possível globalizar qualquer componente de classe pública, para que
esta fique acessível no espaço de nomes global. Globaliza-se uma componente usando a
palavra reservada especial global (cf. secção 11.4).
11 Linguagem OM
219
Se uma componente de classe pública com nome genérico “#g” for globalizada, então ela é
adicionada ao espaço de nomes global com o nome “g”. Não se permitem nomes repetidos no
espaço global. Assim, duas classes distintas não podem globalizar duas componentes que
tenham a mesma denominação.
Uma componente de classe globalizada ganha autonomia face à classe, ou ao modo, onde
foi introduzida. Por isso, toda a parametrização pública implícita na classe, ou modo, tem de
ser copiada para a nova entidade global.
Geralmente, tem interesse globalizar apenas métodos de classe. No entanto, também se permite a globalização de variáveis de classe públicas. A classe gen inclui dois raros exemplos de
variáveis de classe globalizadas: as variáveis fail e repeat, ambas inicializadas com geradores
constantes particulares.
11.5.1 Utilidade
No nível privilegiado, a adição de novas operações primitivas à linguagem pode ser efectuada
por meio de duas técnicas distintas: (1) através da definição de métodos de instância públicos;
(2) através da definição de métodos de classe públicos globalizados.
A primeira técnica é em geral preferível pois evita enriquecer o espaço de nomes global
com demasiados nomes. A biblioteca da linguagem OM segue esta recomendação, dentro do
possível. Eis alguns exemplos de primitivas definidas usando métodos de instância públicos:
identity(), copy(.) , clone(), isNil(), '<=', '+', etc.
Mas os métodos de instância têm três particularidades que em alguns casos são inconvenientes, ou até obstrutivas, relativamente à introdução de certas primitivas: (1) eles têm de ser
invocados usando a notação de envio de mensagem, o.m(…) (questão cosmética apenas); (2) o
receptor da mensagem não pode ser uma referência per se (pois essa referência seria automaticamente desreferenciada); (3) para efeito da aplicação de coerções, existe assimetria no tratamento do receptor face aos argumentos.
Os métodos de classe globalizados não sofrem destes inconvenientes, e foi só por esse motivo que foram introduzidos na linguagem. Para exemplificar:
• A classe $CoreObject globaliza as primitivas checkType[.](.) e downcast[.](.), neste caso apenas por razões cosméticas: pretende-se apenas que a notação introduzida em L4 para
estas primitivas se mantenha;
• A classe $AsgnObject é obrigada a globalizar a primitiva swap(.,.), pois todos os parâmetros desta são referências; cosmeticamente, é também mais elegante escrever swap(a,b)
do que escrever a.swap(b);
• O modo gen introduz uma primitiva de disjunção, '|', a qual é globalizada para que haja
simetria no tratamento dos argumentos: por exemplo, dadas duas expressões a:gen Int e
b:Int, então são válidas as expressões a|b e b|a.
220
OM – Uma linguagem de programação multiparadigma
11.6 Resolução de nomes
A linguagem OM prevê diversos espaços de resolução de nomes, também chamados de níveis
sintácticos:
•
•
•
•
um espaço de nomes global;
um espaço de nomes de instância associado a cada tipo-objecto;
um espaço de nomes de classe associado a cada classe;
um espaço de nomes locais associado a cada função (é possível aninhamento entre espaços de nomes locais pois existe são suportadas funções locais).
Em cada espaço de nomes individual não podem ocorrer nomes repetidos, o que significa
que a linguagem OM não suporta entidades com nomes sobrepostos; ao contrário do C++, por
exemplo, que suporta overloading de métodos.
Num programa, a resolução de nomes só ocorre depois desse programa ter sido sujeito à
tradução descrita na secção 11.3.
A forma como a resolução dum nome n se articula com a exploração dos vários espaços de
nomes é definida pelas seguintes regras:
• Se n ocorrer numa expressão da forma “C#n” ou “#n”, então n é um nome de classe. No
primeiro caso, a resolução de n efectua-se no espaço de nomes de classe da classe C; no
segundo caso, efectua-se no espaço de nomes de classe da classe SELFC.
• Se n ocorrer numa expressão da forma “#$def_select(exp,n)”, então trata-se dum nome de
instância. Neste caso, a resolução de n efectua-se no espaço de nomes de instância
associado ao tipo da expressão exp (note que neste contexto a expressão exp nunca será
sujeita a qualquer coerção, de acordo com o que afirmamos secção 11.3).
• Nos casos restantes, começa-se por tentar resolver o nome n localmente: primeiro no
espaço de nomes locais corrente, e depois, sucessivamente, nos vários espaços de nomes locais envolventes (a linguagem usa escopo estático). Se esta resolução local falhar
então tenta-se resolver o nome n no espaço de nomes de instância do tipo SELFT. Se
esta resolução suceder então n é reescrito como #$def_select(self,n). Finalmente, se
também esta última diligência falhar, é tentado o espaço de nomes global.
São muitas as entidades paramétricas definidas nas classes e nos modos da biblioteca
padrão. O programador também tem a liberdade de introduzir entidades paramétricas nos seus
programas. Devido à abundância de entidades paramétricas, seria desejável que o mecanismo
de resolução de nomes incluisse um módulo de inferência de argumentos de instanciação que
libertasse o programador da necessidade de os explicitar. Esse módulo deveria ser articulado
com o sistema de coerções da linguagem, para que um maior número de alternativas de resolução de nomes pudessem ser considerado: por exemplo, no caso do gerador 1|2 discutido na secção 10.1.3, a descoberta da instanciação para o parâmetro X de gen X envolve necessariamente
a utilização do sistema de coerções.
11 Linguagem OM
221
Limitamo-nos a identificar esta questão da inferência de argumentos de instanciação, pois
não a trataremos nesta dissertação.
11.7 Biblioteca de classes mínima
Apresentamos aqui uma proposta de biblioteca de classes mínima para a linguagem OM.
A biblioteca mínima consiste numa hierarquia de classes, a maioria das quais se dizem
classes primitivas por serem suportadas de forma especial pela infra-estrutura básica da linguagem. A biblioteca mínima não contém qualquer modo predefinido. Assim, as classes da
biblioteca mínima não assumem a existência de qualquer modo.
Na raiz da hierarquia da biblioteca mínima encontra-se a classe primitiva $CoreObject, uma
classe que suporta a funcionalidade básica dos objectos e variáveis, com e sem modo. Um objecto básico possui um método de identidade que permite determinar se dois objectos são o
mesmo. As variáveis do tipo $CoreObject admitem inicialização explícita, mas não inicialização
implícita e atribuição.
A classe Object merece também especial referência pois suporta a funcionalidade genérica
dos objectos e variáveis sem modo. Qualquer objecto da classe Object possui um método de
identidade, identity, e um método que permite verificar se dois objectos são da mesma classe,
sameClasse . As variáveis do tipo Object admitem inicialização implícita, inicialização explícita e
atribuição. Obrigatoriamente, todas as classes não primitivas herdam, directa ou indirectamente, da classe Object.
Apresentamos seguidamente a biblioteca mínima da linguagem OM. Algumas das classes
são apenas esboçadas pois não se justifica gastar muito espaço com certas classes menos importantes. A própria biblioteca mínima inclui alguma da sua documentação mais essencial.
alias defaultInterf = {
#startup :()->(),
coreIdentity:$CoreObject->Bool,
identity:$CoreObject->Bool,
IsNil :()->Bool,
#$def_init :(SAMET&, SAMET)->SAMET,
[R] #$def_select :(SAMET, SAMET->R)->R,
[R] #$def_apply :(SAMET->R, SAMET)->R,
#$def_switch :(SAMET, SAMET->())->(),
#$def_comm :(()->SAMET)->() } ;
alias zeroInterf = defaultInterf +
{ #zero :()->SAMET, #$def_init_implicit :SAMET&->SAMET } ;
alias asgnInterf = zeroInterf +
{ #$swap :(SAMET&,SAMET&)->(),#$def_assign :(SAMET&,SAMET)->SAMET } ;
alias eqInterf = defaultInterf + { '==' :(SAMET)->Bool } ;
alias noInterf = defaultInterf ;
alias objectInterf = defaultInterf + asgnInterf +
{ sameClass :$CoreObject->Bool, copy :Object->SAMET, clone:()->SAMET } ;
//
//
//
//
//
//
A classe $CoreObject suporta a funcionalidade básica
de todos os objectos, com ou sem modo.
Todas as subclasses da classe $CoreObject geram subtipos do tipo $CoreObject.
Esta classe suporta inicialização explicita.
Esta classe não suporta um zero.
Esta classe não suporta inicialização implícita.
222
OM – Uma linguagem de programação multiparadigma
// Esta classe não suporta atribuição.
primitive class $CoreObject {
// Regras especiais
// No modo privilegiado, a atribuição funciona sempre. Se for aplicada
//
a uma variável dum tipo T que não suporte atribuição usa-se nesse
//
caso a inicialização explicita.
// Variáveis
$DYNAMIC_TYPE #$mytype = $BUILD_MY_TYPE() ;
// Literais, constantes e zero
// não tem zero
// Construtores e inicializações
priv primitive SELFT #new()
;
() #startup() {
}
// Serviços base
primitive Bool coreIdentity($CoreObject arg) {
return $SAME_ADRESS(self, arg) ;
}
Bool identity($CoreObject arg)
;
Bool isNil() {
return false ;
}
// Métodos auxiliares
priv [T] Bool checkMyType() {
return #$mytype.subtype(T#$mytype) ;
}
// Componentes globais
global [T] Bool #checkType(SAMET arg) {
return arg.checkMyType[T]() ;
}
global [T] T #downcast(SAMET arg) {
if( checkType[T](arg) )
return $UNCHECKED_RETYPING[T](arg) ;
else raise xConversion ;
}
// Métodos #$def-*
raw SAMET #$def_init(SAMET& var, SAMET val) {
return $raw_init(var, val) ;
}
raw [R] R #$def_select(SAMET obj, SAMET->R label) {
if( obj.isNil() ) raise xNil ;
return $raw_select(obj, label) ;
}
raw [R] R #$def_apply(SAMET->R f, SAMET exp) {
return $raw_apply(f, exp) ;
}
raw () #$def_switch(SAMET exp, SAMET->() body) {
return $raw_switch(exp, body) ;
}
raw () #$def_comm(()->SAMET exp) {
return $raw_comm(exp) ;
}
}
// Relativamente a $CoreObject introduz zero abstracto, inicialização implícita.
primitive class $ZeroObject : $CoreObject {
// Literais, constantes e zero
SAMET #zero() {
raise xAbstract ;
}
11 Linguagem OM
// Métodos #$def-*
raw SAMET #$def_init_implicit(SAMET& var) {
return $def_init(var, SAMET#zero()) ;
}
}
// Relativamente a $ZeroObject introduz a atribuição.
primitive class $AsgnObject : $ZeroObject {
// Componentes globais
global () #swap(SAMET& var1, SAMET& var2) {
SAMET aux = var1 ;
var1 := var2 ;
var2 := aux ;
}
// Métodos #$def-*
raw SAMET #$def_assign(SAMET& var, SAMET val) {
return $raw_assign(var, val) ;
}
}
// A classe Object suporta a funcionalidade básica dos objectos sem modo.
// Directa ou inderectamente, dela herdam todas as classes não-primitivas.
// Todas as subclasses da classe Object geram subtipos do tipo Object.
// As subclasses directas de Object não requerem declaração explícita da
superclasse.
primitive class Object : $AsgnObject {
// Variáveis
Nil $object_with_no_mode ;
// Literais, constantes e zero
SAMET #zero() {
return nil ;
}
// Serviços base
primitive Bool sameClass($CoreObject)
;
macro SAMET copy(Object arg) {
// cópia superficial (shallow copy)
// Os objectos têm de ser da mesma classe!
// Não basta que sejam do mesmo tipo.
if( self.sameClass(arg) ) {
SELFT g = $UNCHECKED_RETYPING[SELFT](arg) ;
MACROForEveryInstVarPair[X](a,b)(self, g) {
if( MACROWellTyped(a := b) ) a := b ;
else #$def_init(a,b)
}
return self ;
}
else raise xClass ;
}
SAMET clone() {
// produz duplicado superficial (shallow clone)
return #new().copy(self) ;
}
}
primitive class Nil : Object {
// Literais, constantes e zero
// literais: nil
// Serviços base
Nil clone() { return nil ; }
Nil copy(Object arg) {
if(arg.isNil()) return nil;
else raise xType;
}
// Métodos específicos
223
224
OM – Uma linguagem de programação multiparadigma
Bool isNil() { return true ; }
}
primitive class $DYNAMIC_TYPE : Object {
// Variáveis
// representação dos tipos-objecto de OM
// Métodos auxiliares
…
// Métodos específicos
Bool subtype($DYNAMIC_TYPE arg) {…}
}
primitive class Equality : Object {
// Métodos específicos
// Esta igualdade por defeito requer que os objs sejam da mesma classe.
// Se esta restrição não interessar à subclasse, ela deve redefinir '=='.
macro Bool '=='(SAMET arg) { // igualdade por defeito: compara todas as vars.
if( self.sameClass(arg) ) {
SELFT g = $UNCHECKED_RETYPING[SELFT](arg) ;
MACROForEveryInstVarPair[X](a,b)(self, g) {
if( MACROWellTyped(a == b) )
{ if( !(a == b) ) return false ; }
else
{ if( !(a.identity(b)) ) return false ; }
}
return true ;
}
else return false ;
}
Bool '!='(SAMET arg) {
return !(self == arg) ;
}
}
class Order : Equality {
// Métodos específicos
Bool '<'(SAMET arg) { raise xAbstract ; }
Bool '<='(SAMET arg) { return !(self > arg) ; }
Bool '>'(SAMET arg) { return arg < self ; }
Bool '>='(SAMET arg) { return !(self < arg) ; }
SAMET max(SAMET arg) { return self > arg ? self : arg ; }
SAMET min(SAMET arg) { return self > arg ? arg : self ; }
}
primitive class Int : Equality {
// Literais, constantes e zero
// literais: 0, 2, -100, 555, …
Int #zero() { return 0 ; }
Int #maxInt = … ;
Int #minInt = … ;
// Métodos específicos
primitive Int '+'(Int n) ;
primitive Int '-'(Int n) ;
primitive Int '-'() ;
primitive Int '*'(Int n) ;
primitive Int '/'(Int n) ;
primitive Int '<<'(Int n) ;
primitive Int '>>'(Int n) ;
…
}
primitive class Float : Equality {
// Literais, constantes e zero
// literais: 0.0, 1.3, -2.6e4, …
11 Linguagem OM
Float #zero() { return 0.0 ; }
Float #fPi = 3.14159265358979323846 ;
Float #fE = 2.71828182845904523536 ;
// Métodos específicos
// aceitam também Int porque Int≤cFloat
primitive Float '-'() ;
primitive Float log() ;
primitive Float cos() ;
primitive Float #Float_from_Int_conversion(Int i) ;
…
// Componentes globais
// aceitam também Int porque Int≤cFloat
global primitive Float #'+'(Float a, Float b) ;
global primitive Float #'-'(Float a, Float b) ;
global primitive Float #'*'(Float a, Float b) ;
global primitive Float #'/'(Float a, Float b) ;
global primitive Float #power(Float a, Float b) ;
// Coerções
coercion Float #Float_from_Int(Int arg) {
return #Float_from_Int_conversion(arg) ;
}
}
primitive class Bool : Equality {
// Literais, constantes e zero
// literais: false, true
Bool #zero() { return false ; }
// Métodos específicos
primitive Bool '&&'(Bool ()->b) ;
primitive Bool '||'(Bool ()->b) ;
primitive Bool '!'() ;
…
// Métodos #$def-*
raw [X,Y] () #$def_ifthenelse(Bool cond, ()->X thenP, ()->Y elseP) {
return $raw_ifthenelse(cond, thenP, elseP) ;
}
raw [X] () #$def_while(()->Bool cond, ()->X body) {
return $raw_while(cond, body) ;
}
raw [X] () #$def_dowhile(()->X body, ()->Bool cond) {
return $raw_dowhile(body, cond) ;
}
raw [X] X #$def_cond(Bool cond, ()->X thenP, ()->X elseP) {
return $raw_cond(cond, thenP, elseP) ;
}
}
primitive class Char : Equality {
// Literais, constantes e zero
// literais: '\0', '\n', '\'', '\"', 'a', 'b', 'c', …
Char #zero() { return '\0' ; }
// Métodos específicos
primitive Char succ() ;
primitive Char pred() ;
primitive Char toUpper() ;
primitive Int code() ;
…
}
primitive class Str : Equality {
// Literais, constantes e zero
// literais: "", "ola", "om", "sol", …
225
226
OM – Uma linguagem de programação multiparadigma
Str #zero() { return "" ; }
// Métodos específicos
primitive Str '+'(Str s) ;
// concatenação
primitive Str slice(Int i, Int j) ;
primitive Str includes(Char c) ;
primitive Char& '[]'(Int i) ;
…
}
alias arrayInterf = asgnInterf + zeroInterf ;
primitive class $Array[T < arrayInterf] : Equality {
// Notação especial
// T[] = $Array[T]
// Literais, constantes e zero
// literais (usa-se a notação do C++): {}, {1,5,7}, …
T[] #zero() { return {} ; }
// Métodos específicos
primitive T& '[]'(Int i)
;
[R] R[] map(T->R f) { … }
…
}
primitive class $Fun[T,R] : Object {
//
//
//
//
//
//
//
Notação especial
(T1,…,Tn)->R = $Fun[T1,$Fun[…,$Fun[Tn,R]…]]
(T1,…,Tn)->() = $Fun[T1,$Fun[…,$Fun[Tn,$Void]…]]
T->R = $Fun[T,R]
T->() = $Fun[T,$Void]
()->R = $Fun[$Void,R]
()->() = $Fun[$Void,$Void]
// Literais, constantes e zero
// literais: (R)fun(T arg){ return arg ; } ()fun(){ return ; }
T->R #zero() { return (R)fun(T arg){ return arg ; } ; }
// Métodos específicos
[S] (T->S) compose(R->S f) { … }
…
}
primitive class $Void : $CoreObject {
// Notação especial
// () = $Void
//
//
//
//
Regras especiais
Uma função f de tipo ()->X é invocada usando a notação especial f().
Dentro de fun do tipo X->() existe a forma expecial exclusiva “return ;”
()=$Void não pode ser usado para instanciar entidades paramétricas
// Literais, constantes e zero
// literais: não tem -> repare: o termo () não é suportado em OM!
// não há zero
// Métodos #$def-*
raw () #$def_nop() {
return $raw_nop() ;
}
raw [X,Y] () #$def_seq(()->X c1, ()->Y c2) {
return $raw_seq(c1, c2) ;
}
}
primitive class $Ref[V] : $CoreObject {
11 Linguagem OM
// Notação especial
// V& = $Ref[V]
// Literais, constantes e zero
// literais: todos os identificadores de variável e parâmetros de função
// não há zero
// Coerções
coercion V #Value_from_Ref(V& var) {
return $raw_deref(var) ;
}
}
// A classe Exception é primitiva apenas para não ser subclasse de Object.
//
O modo gen suporta geradores de referências: gen X&
primitive class Exception : $CoreObject {
// Variáveis
priv Str msg ;
// Construtores e inicializações
priv SELFT #new(Str str) {
SELFT s = #new() ;
s.msg := str ;
return s ;
}
// Literais, constantes e zero
// não tem literais
// não há zero
Exception #xAbort = #new("abort") ;
Exception #xNil = #new("nil accessed") ;
Exception #xDivByZero = #new("division by zero") ;
Exception #xAbstract = #new("abstract method called") ;
Exception #xType = #new("bad type") ;
Exception #xClass = #new("bad class") ;
Exception #xDowncast = #new("bad downcast") ;
Exception #xNotConneted = #new("connected object not available") ;
…
// Métodos #$def-*
raw () #$def_raise(Exception exc) {
return $raw_raise(exc) ;
}
raw [X,Y] () #$def_trywith(()->X comm, Exception exc, ()->Y do) {
return $raw_trywith(comm, exc, do) ;
}
}
227
Capítulo 12
Modos da biblioteca padrão
Neste capítulo apresentamos os cinco modos predefinidos que decidimos incluir na biblioteca
padrão da linguagem OM. Genericamente, eles são ortogonais entre si, exceptuando o modo
value que obriga todos os outros modos, presentes e futuros, a tomarem-no em consideração.
O modo const suporta a introdução de identificadores que denotam valores constantes.
O modo value introduz semântica de não-partilha na linguagem.
O modo lazy suporta avaliação preguiçosa, uma técnica que permite adiar a avaliação duma
expressão até ao momento em que o valor dessa expressão é realmente necessário.
O modo log introduz variáveis lógicas e unificação sintáctica e semântica.
O modo gen introduz retrocesso (backtraking) na linguagem, pela via da noção de gerador.
12.1 Componentes adicionadas a $CoreObject
As definições dos modos value e gen requerem a introdução de diversas componentes novas na
classe $CoreObject, assim como a modificação de dois dos métodos “#$def_*”. Apresentamos seguidamente essas modificações, as quais só poderão ser compreendidas quando estudadas em
ligação com os modos que as requerem.
primitive class $CoreObject {
// Componentes adicionadas
// Variáveis
Bool #$expanded ; // para value
priv $OBJ #$trailTop = nil ; // para gen
priv $OBJ $trailPrev ; // para gen
// Construtores e inicializações
() #startup() {
#$expanded := false ;
}
// Serviços para outros modos
raw SAMET $dup(){ // para value
return self ;
}
$CoreObject #getTrailLevel() { // para gen
return #$trailTop ;
}
() #restoreTrail($CoreObject l) { // para gen
230
OM – Uma linguagem de programação multiparadigma
while( ! #$trailTop.orig_ident(l) ) {
#$trailTop.onBack() ;
#$trailTop := #$trailTop.$trailPrev ; } ;
}
}
() trail() { // para gen
$trailPrev := #$trailTop ;
#$trailTop := self ;
}
() onBack() { // para gen
raise xAbstract ;
}
// Métodos #$def-*
raw SAMET #$def_init(SAMET& var, SAMET val) { // para value
SAMET arg := #$expanded ? val.$dup() : val ;
return $raw_init(var, arg) ;
}
raw R #$def_apply(T->R f, T exp){ // para value
T arg := T#$expanded ? exp.$dup() : exp ;
R res := $raw_apply(f, arg) ;
return R#$expanded ? res.$dup() : res ;
}
}
12.2 Modo const
O modo const permite que valores constantes possam ser denotados por identificadores.
O modo const influência as propriedades duma única categoria de entidades tipificadas: as
variáveis. Relativamente às restantes categorias de entidades tipificadas, o modo const é neutral.
As propriedades essenciais das variáveis com modo const são duas: (1) essas variáveis têm
de ser inicializadas no ponto de declaração (quer sejam variáveis locais, de instância ou de
classe); (2) elas não podem ser alvo do operador de atribuição.
Prova-se facilmente que o modo const verifica a propriedade: τ≤σ ⇒ const τ≤const σ.
A realização da parte dinâmica do modo const é trivial, quase idêntica à realização do modo
neutral apresentado no final da secção 9.1.4. A realização da parte estática deste modo é igualmente trivial, já que é simplesmente determinada pela não definição deliberada dos dois seguintes métodos: #$def_init_defaut e #$def_assign.
alias constInterf = defaultInterf ;
mode const[T < constInterf] :$CoreObject {
// Variáveis
Nil $object_with_mode_const ;
priv T obj ;
// Literais, constantes e zero
// não tem zero
// Construtores e inicializações
() #startup() { // para value
#$expanded := T#$expanded ;
}
12 Modos da biblioteca padrão
231
priv SELFT #new(T arg) {
SELFT s = #new() ;
s.obj := arg ;
return s ;
}
// Serviços base
Bool identity($CoreObject arg) {
return checkType[SAMET](arg) && obj.identity(arg.$access()) ;
}
T $access() {
return obj ;
}
// Serviços para outros modos
raw SAMET $dup(){ // para value
SELFT s = #new() ;
s.obj := obj.$dup() ;
return s ;
}
// Métodos auxiliares
//
// Métodos específicos
//
// Componentes globais
//
// Coerções
coercion const T #const_up(T arg) {
return #new(arg) ;
}
coercion T #const_down(const T arg) {
return arg.$access() ;
}
// T ≤extra const T
// const T ≤extra T
// Métodos #$def-*
//
}
12.3 Modo value
O modelo de manipulação de dados básico da linguagem OM é um modelo que se diz ser de
partilha, pois nele os objectos básicos possuem uma identidade podendo um objecto ser partilhado por duas variáveis distintas.
O modo value introduz um modelo de manipulação de dados baseado em valores, i.e. em
elementos de dados sem identidade e não-partilháveis. Neste modelo, sempre que um valor é
usado ele é implicitamente duplicado: por exemplo a atribuição x:=y liga x a uma cópia do
valor da variável y; também a aplicação f(y) obriga a duplicar o valor da variável y no momento
da passagem do parâmetro. A única situação em que um valor é usado sem ser duplicado
ocorre no caso dum objecto que é alvo da aplicação dum método: por exemplo, a expressão
y.m() não duplica o objecto a que a variável y está ligada.
Um modelo baseado em valores é útil para tratar situações em que um ou mais objectos não
devam ser partilhados: por exemplo, na representação dum automóvel, cada uma das suas
232
OM – Uma linguagem de programação multiparadigma
quatro rodas constitui um subobjecto que não deve ser partilhado com qualquer outro automóvel.
As variáveis com modo value suportam inicialização implícita e atribuição. Como a inicialização implícita é suportada, o modo value só pode ser instanciado com classes que definam o
método de classe #zero(). Quando aplicado a objectos com modo value, o método de identity
retorna sempre false.
O modo value verifica a propriedade: τ≤σ ⇒ value τ≤value σ.
Do ponto de vista da realização este é um modo de biblioteca bastante complexo e intrusivo. Obriga a redefinir os métodos #$def_init e #$def_apply ao nível da classe $CoreObject, requer a
introdução da variável de classe #$expanded também na classe $CoreObject, e, finalmente, exige a
colaboração de todos os outros modos, os quais têm a responsabilidade de inicializar a variável
#$expanded e de definir um método de duplicação propagada chamado $dup.
Num modo composto que tenha value por componente, a influência de value fica localizada
no modo atómico que imediatamente lhe sucede. Concretamente, a propriedade da não-partilha fica associada aos objectos conexos (cf. secção 9.1.4) do modo atómico que sucede a value.
Para exemplificar, consideremos o modo composto value log. Este modo composto suporta a
operação de atribuição. Se x e y forem variáveis com modo value log, o efeito da atribuição x:=y
é o seguinte:
• Se y for uma variável livre então x torna-se numa variável livre independente (não-partilhada);
• Se y tem valor, então x torna-se numa variável independente (sem partilha), instanciada
com uma cópia do valor de x;
• Nestes dois casos, o valor de x é alterado e não será reposto quando ocorrer retrocesso.
Para terminar, vejamos como é que os modos const, lazy e log lidam com a existência de dois
modelos de manipulação de dados. Os modos const e lazy respeitam o modelo de manipulação
associado ao tipo usado na sua instanciação, qualquer que seja o modelo usado: assim, por
exemplo, os modos compostos const value , lazy value ou const lazy value regem-se pelo modelo
baseado em valores. Quanto ao modo log, este força sempre a utilização do modelo básico de
partilha: por exemplo, tomando duas variáveis lógicas não instanciadas mas mutuamente ligadas, basta instanciar uma delas para que a outra fique imediatamente ligada ao mesmo valor.
alias valueInterf = zeroInterf ;
mode value[T < valueInterf] : $AsgnObject {
// Variáveis
Nil $object_with_mode_value ;
priv T obj ;
// Literais, constantes e zero
SAMET #zero() {
return #new(T#zero()) ;
}
12 Modos da biblioteca padrão
233
// Construtores e inicializações
() #startup() { // para value
#$expanded := true ;
}
priv SELFT #new(T arg) {
SELFT s = #new() ;
s.obj := arg ;
return s ;
}
// Serviços base
Bool identity($CoreObject arg) {
return false ;
}
T $access() {
return obj ;
}
// Serviços para outros modos
raw SAMET $dup(){ // para value
SELFT s = #new() ;
s.obj := obj.$dup() ;
return s ;
}
// Métodos auxiliares
//
// Métodos específicos
//
// Componentes globais
//
// Coerções
coercion value T #value_up(T arg) {
return #new(arg) ;
}
coercion T #value_down(value T arg) {
return arg.$access() ;
}
// T ≤extra value T
// value T ≤extra T
// Métodos #$def-*
raw value T #$def_assign((value T)& var, value T val){
return $raw_assign(var, val.$dup()) ;
}
}
12.4 Modo lazy
O modo lazy suporta avaliação preguiçosa, uma técnica que permite adiar a avaliação duma
expressão até ao momento em que o valor dessa expressão é realmente necessário. O modo
lazy é útil para definir estruturas de dados infinitas. O modo lazy também permite declarar parâmetros de funções que devam ser avaliados de forma preguiçosa, o que permite obter uma
variante de passagem de parâmetros call-by-name.
As variáveis com modo lazy são obrigatoriamente inicializadas no ponto de declaração e
não podem ser alvo de atribuição. Em retrocesso, o estado de avaliação adiada dum objecto
lazy é reposto.
O modo lazy verifica a propriedade: τ≤σ ⇒ lazy τ≤lazy σ.
234
OM – Uma linguagem de programação multiparadigma
Usando o modo lazy, eis uma forma elegante de definir o gerador infinito que produz a sequência de todos os números naturais:
lazy gen Int nats = 0 | (nats + 1)
Eis uma forma alternativa, de definir o mesmo gerador sem usar o modo lazy:
Int i ;
gen Int nats = (i:= 0) | repeat & (i:= i+1)
alias lazyInterf = defaultInterf ;
mode lazy[T < lazyInterf] :$CoreObject {
// Variáveis
Nil $object_with_mode_lazy ;
priv ()->T f ;
priv Bool hasValue ;
priv T obj ;
// Literais, constantes e zero
// não tem zero
// Construtores e inicializações
() #startup() { // para value
#$expanded := T#$expanded ;
}
priv SELFT #new(()->T arg) {
SELFT s = #new() ;
s.f := arg ;
s.hasValue := false ;
return s ;
}
priv SELFT init(T arg) {
obj := arg ;
hasValue := true ;
trail() ;
return self ;
}
priv SELFT initz() {
hasValue := false ;
return self ;
}
// Serviços base
Bool identity($CoreObject arg) {
return coreIdentity(arg) ;
}
}
T $access() {
if( !hasValue ) init(f()) ;
return obj ;
}
// Serviços para outros modos
raw SAMET $dup(){ // para value
SELFT s = #new() ;
s.f := f ;
s.hasValue := hasValue ;
if( hasValue ) s.obj := obj.$dup() ;
return s ;
}
() onBack() { // para gen
initz() ;
}
// Métodos auxiliares
12 Modos da biblioteca padrão
235
//
// Métodos específicos
//
// Componentes globais
//
// Coerções
coercion lazy T #lazy_up(()->T arg) {
return #new(arg) ;
}
coercion T #lazy_down(lazy T arg) {
return arg.$access() ;
}
// ()->T ≤extra lazy T
// lazy T ≤extra T
// Métodos #$def-*
//
}
12.5 Modo log
O modo log já foi alvo de apresentação preliminar na secção 9.1.2. Este modo introduz as
noções de variável lógica e unificação, e as operações específicas isVar e '=='. O modo log não
suporta qualquer mecanismo especial de avaliação baseado em retrocesso: é o modo gen o
modo responsável por esse mecanismo.
As variáveis com modo log não suportam atribuição, mas suportam inicialização implícita:
as variáveis lógicas são implicitamente inicializadas como variáveis livres. O modo value só
admite ser instanciado com classes que suportem uma igualdade do tipo '==':SAMET->Bool.
O modo log não verifica a propriedade: τ≤σ ⇒ log τ≤log σ. A razão é simples: a definição
deste modo inclui uma ocorrência negativa de SAMET na assinatura do método '=='.
A igualdade '==' definida nos objectos lógicos implementa uma operação de unificação
[Hog94]. Só a parte superficial desta operação é especificada dentro do modo log: trata-se da
parte correspondente aos pares variável-variável, variável-objecto e objecto-variável. O par
restante, objecto-objecto, é tratado no método de igualdade que está definido nos objectos
conexos do modo log.
Note que a definição por defeito de '==' na classe Equality, produz o tradicional algoritmo de
unificação sintáctica, já que, como se pode observar na biblioteca de classes, quando está em
causa um par objecto-objecto, este método compara recursivamente as componentes respectivas dos dois objectos envolvidos. Contudo, redefinindo '==' nas classes desejadas, é possível
obter um algoritmo de unificação semântica específico dessas classes.
Este nosso algoritmo de unificação omite a verificação occur-check [Hog94]. Assim, expressões como I==#f(I), onde I é uma variável lógica e #f um construtor, podem dar origem a
estruturas cíclicas, relativamente às quais o programador se deve acautelar.
Em OM, é possível simular com eficácia a funcionalidade dos termos da linguagem Prolog.
Um functor Prolog representa-se usando uma classe paramétrica C com as seguintes caracterís-
236
OM – Uma linguagem de programação multiparadigma
ticas: C é uma subclasse de Equality; C inclui um construtor público; se n for a aridade do functor, n é o número de variáveis de instância públicas com modo log a incluir a classe C. Por
exemplo, o functor f/3 pode ser simulado usando a classe:
class f3[T1<logInterf, T2<logInterf, T3<logInterf] : Equality {
log T1 x1 ; log T2 x2 ; log T3 x3 ;
#create(log T1 a1, log T2 a2, log T3 a3) {
x1 == a1 ; x2 == a2 ; x3 == a3 ;
}
}
Eis a tradução para OM do termo-prolog f(1, 2.5, "ola") (assumindo que se realizou inferência
dos argumentos de instanciação T1, T2 e T3):
f3#create(1, 2.5, "ola")
alias logInterf = eqInterf ;
mode log T[T < logInterf] :$ZeroObject {
// Variáveis
Nil $object_with_mode_log ;
T obj ;
Bool hasValue ;
SAMET next ;
// Literais, constantes e zero
SAMET #zero() {
return #new().initz() ;
}
// Construtores e inicializações
() #startup() { // para value
#$expanded := false ;
}
priv SELFT #new(T arg) {
SELFT s = #new() ;
s.init(arg) ;
return s ;
}
priv SELFT init(T arg) {
obj := arg ;
hasValue := true ;
next := nil ;
trail() ;
return self ;
}
priv SELFT initz() {
hasValue := false ;
next := nil ;
return self ;
}
priv SELFT initv(SAMET arg) {
if( !coreIdentity(arg) ) {
next := arg ;
trail() ;
}
return self ;
}
// Serviços base
Bool identity($CoreObject arg) {
if( checkType[SAMET](arg) ) {
// se não forem já iguais então …
12 Modos da biblioteca padrão
237
SAMET s = drf(), a = arg.drf() ;
if( s.hasValue && a.hasValue )
return s.obj.identity(a.obj) ;
else
return a.coreIdentity(s) ;
}
else return false
}
T $access() {
SAMET s = drf() ;
if( s.hasValue ) return s.obj ;
else raise xNotConneted ;
}
// Serviços para outros modos
raw SAMET $dup(){ // para value
SELFT s = #new() ;
SAMET d = drf() ;
s.hasValue := d.hasValue ;
s.next := nil ;
if( s.hasValue ) s.obj := d.obj ;
return s ;
}
() onBack() { // para gen
initz() ;
}
// Métodos auxiliares
SAMET drf() {
return next == nil ? self : next.drf() ;
}
// Métodos específicos
Bool '=='(SAMET arg) {
SAMET s = drf(), a = arg.drf() ;
if( s.hasValue )
if( a.hasValue )
return s.obj == a.obj ;
else { a.init(nil) ; $raw_assign(a.obj,s.obj) ; } // para value
else
if( a.hasValue )
{ s.init(nil) ; $raw_assign(s.obj,a.obj) ; } // para value
else s.initv(a) ;
return true ;
}
Bool isVar() {
return !drf().hasValue ;
}
// Componentes globais
//
// Coerções
// Não existe a regra extra log T ≤extra T
coercion log T #log_up(T arg) {
return #new(arg) ;
}
coercion gen T #gen_from_log(log T arg) {
try return single(arg.$access()) ;
with(xNotConneted) return fail ;
}
// T ≤extra log T
// log T ≤extra gen T
// Métodos #$def-*
// As coerções não bastam no caso seguinte pois a definição original de
// #$def_select abrange este caso, com semântica indesejada.
// Sem esta definição o.x poderia produzir a excepção "xNotConneted" em vez
//
de falhar.
raw [R] gen R #$def_select(log T o, T->R label) {
return (gen T)#$def_select(#gen_from_log(o), label) ;
}
238
OM – Uma linguagem de programação multiparadigma
}
12.6 Modo gen
Pela via da noção de gerador, e um pouco à maneira da linguagem Icon [Gri83], o modo gen
introduz a possibilidade de usar retrocesso (backtracking) na avaliação das expressões da linguagem. O modo gen colabora com o modo log no suporte do paradigma lógico pela linguagem
OM.
Deliberadamente, as variáveis com modo gen não suportam inicialização implícita nem atribuição directa.
O modo gen verifica a propriedade: τ≤σ ⇒ gen τ≤gen σ.
Entre as numerosas componentes específicas do modo gen incluem-se as conhecidas operações da linguagem Prolog: repeat, fail, ‘&’, '|', '~'.
Cut, o mecanismo não estruturado de controlo da linguagem Prolog, não é suportado pelo
modo gen. Esta ausência resulta tanto duma decisão de concepção (preferimos usar um mecanismo de controlo estruturado), como duma questão de praticabilidade. A alternativa ao uso do
cut, fomos buscá-la à linguagem UNL-Prolog [PPT87], uma linguagem que propõe a substituição do cut por uma colecção de predicados de controlo de alto nível (e também um mecanismo
de implicação exclusiva que não aproveitamos aqui). O modo gen suporta a maioria dos predicados de controlo de alto nível da linguagem UNL-Prolog, nomeadamente: possible, sideeffects,
once, dowhile e until .
Com a introdução do modo gen passam a haver duas formas de descrever computações: (1)
usando comandos, expressões e iteração; (2) usando geradores e retrocesso. Para conciliar
estas duas formas, introduzimos as seguintes três primitivas: every – dado um gerador e um
comando, gera todos os valores do gerador e executa o comando para cada um deles; freegen –
a partir dum gerador normal cria um gerador especial que pode ser explorado usando iteração
em diversos pontos do programa, à maneira das co-expressions da linguagem Icon [Gri83];
parallel – a partir de dois geradores cria um novo gerador que permite explorar em paralelo os
dois geradores originais, usando retrocesso.
A realização da parte dinâmica do modo gen é complexa e foi efectuada usando continuações [Sto77]. Cada gerador é codificado numa função com dois argumentos: o primeiro argumento é a continuação-principal que representa toda a actividade futura do programa em caso
de sucesso; o segundo argumento é uma continuação-alternativa que representa toda a actividade futura do programa em caso de falhanço. Quando uma continuação-principal é activada,
propagamos através dela o último resultado intermédio gerado e a continuação-alternativa, a
qual só será activada em caso de falhanço da continuação-principal.
12 Modos da biblioteca padrão
239
Na nossa implementação todas as continuações-alternativa são criadas dentro do método
thenelse do modo gen. A nossa codificação de geradores tem algumas semelhanças com as codificações, apresentadas em [MH83], [Sch86], de algumas facetas da linguagem Prolog.
Para registar os objectos cujo estado deve ser reposto em retrocesso, introduzimos na classe
$CoreObject uma pilha de objectos designada por trail. Em caso de retrocesso, o método thenelse
activa o procedimento de restauração #restoreTrail, o qual envia a mensagem onBack para todos
os objectos cujo estado deva ser restaurado.
alias genInterf = defaultInterf ;
mode gen [T < genInterf] :$CoreObject {
alias Alt = ()->() ;
alias Cont[X] = (X, Alt)->() ;
alias CA[X] = (Cont[X], Alt)->() ;
alias PCA[X] = (X, Cont[X], Alt)->() ;
// Variáveis
Nil $object_with_mode_gen ;
priv CA[T] objGen ;
// Literais, constantes e zero
// não tem zero
priv CA[T] #ca0 =
()fun(Cont[T] _, Alt _){} ;
priv Cont[T] #k0 =
()fun(T _, Alt _){} ;
priv Alt #a0 =
()fun(){} ;
// Construtores e inicializações
() #startup() { // para value
#$expanded := false ;
}
priv SELFT #new(CA[T] ca) {
SELFT s = #new() ;
s.objGen := ca ;
return s ;
}
CA[T] getCA() {
return objGen ;
}
// Serviços base
Bool identity($CoreObject arg) {
return coreIdentity(arg) ;
}
T $access() {
if( first(.t) ) return t ;
else raise xNotConneted ;
}
// Serviços para outros modos
raw SAMET $dup(){ // para value
SELFT s = #new() ;
s.objGen := objGen ;
return s ;
}
// Métodos auxiliares
priv CA[T] thenelse(PCA[T] g1, CA[T] g2) {
$CoreObject l = #getTrailLevel() ;
return
()fun(Cont[T] k, Alt a){
self.objGen(()fun(T t, Alt al){ g1(t,k,al) ; },
240
OM – Uma linguagem de programação multiparadigma
()fun(){ #restoreTrail(l) ; g2(k,a) ; }
) ;
} ;
}
priv Bool first(T& var) {
Bool res = false ;
self.thenelse(
()fun(T t, Cont[T] _, Alt _){ var := t ; res := true ; },
#ca0)(#k0, #a0) ;
return res ;
}
// Métodos específicos
//
// Componentes globais
global Bool #check(gen T g) {
return g.first(.z) ;
}
global gen T #single(()->T t) {
return #new(
()fun(Cont[T] k, Alt a){
T v = t() ;
// false = fail
if( !v.coreIdentity(false) ) k(v,a) ; else a() ; }
}) ;
}
global gen T #fail =
#new( ()fun(Cont[T] _, Alt a){ a() ; } ) ;
global gen Nil #repeat =
single(nil) | repeat ;
global [X < genInterf] gen T '#&'(gen X g1, gen T g2) {
return #new(
()fun(Cont[T] k, Alt a){
g1.getCA()(()fun(X _, Alt al){ g2.getCA()(k, al) ; },
a) ;
}) ;
}
global gen T '#|'(gen T g1, gen T g2) {
return #new(
g1.thenelse(()fun(T t, Cont[T] k, Alt a){ k(t,a) ; },
g2.getCA())
) ;
}
global gen Nil '#~'(gen T g) {
return check(g) ? fail : single(nil) ;
}
global gen Nil #possible(gen T g) {
return ~~g ;
}
global gen Nil #sideeffects(gen T g) {
check(g) ;
return single(nil) ;
}
global gen T #once(gen T g) {
return g.first(.t) ? single(t) : fail ;
}
global [X < genInterf] gen T #dowhile(gen T g, gen X gc) {
return #new(
()fun(Cont[T] k, Alt a){
g.getCA()(()fun(T t, Alt aa)
{ if( check(gc) ) k(t,aa) ; else a()) ; },
a) ;
}) ;
}
global [X < genInterf] gen T #dowhile2(gen X g, gen T gc) {
return #new(
()fun(Cont[T] k, Alt a){
g.getCA()(()fun(X _, Alt aa)
{ if(gc.first(.t) ) k(t,aa); else a()) ; },
a) ;
12 Modos da biblioteca padrão
241
}) ;
}
global [X < genInterf] gen T #until(gen T g, gen X gc) {
return #new(
()fun(Cont[T] k, Alt a){
g.getCA()(()fun(T t, Alt aa)
{ if( check(gc) ) k(t,a) ; else k(t,aa) ; },
a) ;
}) ;
}
global [X] () #every(gen T g, ()->X body) {
g & single(body) & fail ;
}
global gen T #freegen(gen T g){
gen T aux ;
// aux e gv vars locais de closure
()->() gv = ()fun() {
g.thenelse(
()fun(T t, Cont[T] _, Alt aa){
aux := single(t) ; gv := ()fun() { a() ; } ; },
()fun(Cont[T] _, Alt _){ aux := fail ; gv:=()fun() { ; } ; })
(#k0,#a0) ;
} ;
return #new(()fun(Cont[T] k, Alt a){ gv() ; aux(k, a) ; }) ;
}
global [X < genInterf] gen T #parallel(gen X g1, gen T g2) {
return dowhile2(g1, freegen(g2)) ;
}
// Coerções
// Não existe a regra extra gen T ≤extra T
coercion (T->gen T) #gen_from_pred(T->Bool f) {
// T->Bool ≤extra T->gen T
return
(gen T)fun(T t){ return (f(t) ? single(t) : fail) ; }
}
coercion gen T #gen_up(()->T arg) {
// ()->T ≤extra gen T
return single(arg) ;
}
// Métodos #$def-*
[X] gen T #$def_cond(gen T gc, ()->X thenP, ()->X elseP) {
return check(gc) ? thenP() : elseP() ;
}
[X,Y] () #$def_ifthenelse(gen T gc, ()->X thenP, ()->Y elseP) {
if( check(gc) ) then thenP() else elseP() ;
}
[X] () #$def_while(()->(gen T) gc, ()->X body) {
while( check(gc()) ) body() ;
}
[X] () #$def_dowhile(()->X body, ()->(gen T) gc) {
do body() while( check(gc()) ) ;
}
() #$def_switch(gen T g, T->() body) {
if( g.first(.t) )
switch( t ) body() ;
}
[R] gen R #$def_select(gen T go, T->R label) {
return #new(
()fun(Cont[R] k, Alt a){
go.getCA()(()fun(T o, Alt aa){
k(T#$def_select(o,label),aa) ; },
a) ;
}) ;
}
() #$def_comm(()->(gen T) g) {
check(g()) ;
}
global () #$def_init(T& var, gen T g) {
// a variável não é alterada se o gerador falhar
if( g.first(.t) ) T#$def_init(var,t) ;
}
242
OM – Uma linguagem de programação multiparadigma
global [X < genInterf+asgnInterf] gen X #$def_assign(gen (X&) gv, gen X g) {
return #new(
()fun(Cont[X] k, Alt a){
gv.getCA()(()fun(X& v, Alt aa){
g.getCA()(()fun(X x, Alt aaa){
k(v:=x, aaa) ; },
aa),
a) ;
}) ;
}
global [R] gen R #$def_apply(gen (T->gen R) gf, gen T g){
return #new(
()fun(Cont[R] k, Alt a){
gf.getCA()(()fun(T->gen R f, Alt aa){
g.getCA()(()fun(T t, Alt aaa){
(f t).getCA()(k,aaa) ; },
aa),
a) ;
}) ;
}
}
12.6.1 Protótipo do modo gen
O modo gen é tão complexo que tivemos a necessidade de escrever um protótipo para validar
as nossas ideias e corrigir os erros. Eis o protótipo que escrevemos em CAML:
type trvar = { prev:trvar; var: int ref ; } ;;
let rec varbase = { prev=varbase; var=ref 0 } ;;
let vars = ref varbase ;;
let rec trail v = (vars := { prev= !vars; var=v }) ;;
and getlevel () = !vars ;;
and restoreuntil level =
if !vars == level then ()
else ( !vars.var := -1 ; vars := !vars.prev ; restoreuntil level )
let
let
let
let
ca0 = fun k aa-> () ;;
k0 = fun x aa-> () ;;
a0 = fun ()-> () ;;
z0 = ref 0 ;;
let rec bifthenelse gc g1 g2 k a =
let h = getlevel() in
gc (fun n aa-> g1 n k aa) (fun ()-> restoreuntil h ; g2 k a)
and first g var =
let res = ref false in
bifthenelse g (fun n _ _->var:=n; res:=true) ca0 k0 a0 ;
!res
and bcheck g =
first g z0
and bsingle n k =
k n
and bvar v k =
k !v
and bfail k a =
a ()
and brepeat k =
bor (bsingle (-1)) brepeat k
and band g1 g2 k =
g1 (fun _->g2 k)
and bor g1 g2 k =
bifthenelse g1 (fun n->k n) g2 k
and borlist l k =
match l with
12 Modos da biblioteca padrão
[] -> (bfail) k
| (x::xs) -> (bor (bsingle x) (borlist xs)) k
and bif gc g1 g2 =
if first gc z0 then g1 else g2
and bnot g =
if first g z0 then bfail else (bsingle (-1))
and bpossible g =
bnot (bnot g)
and bsideeffects g =
first g z0 ; (bsingle (-1))
and bonce g =
if first g z0 then (bsingle !z0) else bfail
and bdowhile g gc k a =
g (fun n aa-> if first gc z0 then (k n) k aa else bfail k a) a
and bdowhile2 g gc k a =
g (fun _ aa-> if first gc z0 then (k !z0) k aa else bfail k a) a
and buntil g gc k a =
g (fun n aa-> if first gc z0 then (k n) k a else (k(t,a) n) k aa) a
and bevery g q =
bsideeffects (band g (band q bfail))
and bfreegen g =
let aux = ref bfail in
let rec gv = ref (fun ()->
bifthenelse
g
(fun n _ aa-> aux:=(bsingle n); gv:=fun ()->aa ())
(fun _ _-> aux:=bfail; gv:=fun ()->())
k0
a0)
in (fun k a-> !gv (); !aux k a)
and bpar g1 g2 =
bdowhile2 g1 (bfreegen g2)
and basgn v g k =
g (fun n aa-> v:=n; k n aa)
and brasgn v g k =
g (fun n aa-> trail(v); v:=n; k n aa)
and bprint g k =
g (fun n aa-> print_int n ; print_newline(); k n aa)
and run g =
(band g bfail) k0 a0
and runp g =
run (bprint g)
;;
(*
runp
runp
runp
runp
runp
runp
runp
runp
(bsingle 5) ;;
(bfail) ;;
(band (bsingle 5) (bsingle 7)) ;;
(bor (band (bsingle 5) (bsingle 7)) (bsingle 6)) ;;
(let v = ref 0 in bor (basgn v (bsingle 6)) (bvar v)) ;;
(let v = ref 0 in bor (brasgn v (bsingle 6)) (bvar v)) ;;
(btry (bsingle 5)) ;;
(btry bfail) ;;
let rr = (borlist [9; 10; 11; 12; 13]) ;;
let ss = (borlist [21; 22; 23]) ;;
run (bor (bprint rr) (bprint ss)) ;;
run (band (bprint rr) (bprint ss)) ;;
run (bpar (bprint rr) (bprint ss)) ;;
runp (bpar rr ss) ;;
*)
243
Capítulo 13
Exemplo
A linguagem de programação OM tem diversos aspectos positivos:
• Tem um mecanismo de herança flexível que convive pacificamente com a relação de
subtipo (a expressividade da linguagem depende bastante deste aspecto);
• Introduz o mecanismo dos modos, um mecanismo de meta-nível que foi possível fazer
surgir de forma natural no contexto duma teoria de objectos e classes, e cujas potencialidades foram exploradas na construção da linguagem OM.
• Está bem fundamentada teoricamente, e de uma forma razoavelmente simples e abordável.
No entanto, esquecendo as questões técnicas e a elegância formal da forma como foi introduzida, não se pode dizer que a versão actual da linguagem OM seja particularmente original
no que diz respeito aos paradigmas suportados. Efectivamente, os paradigmas que escolhemos
para ilustrar as capacidades de extensão da linguagem foram os sempre habituais: paradigma
orientado pelos objectos, paradigma funcional e paradigma lógico.
De qualquer forma, justifica-se que apresentemos um exemplo de dimensão média que ilustre uma aplicação concreta da linguagem e ajude a esclarecer se o nosso método de integrar
paradigmas deu, ou não, origem a uma linguagem de utilidade prática. É isso o que faremos no
presente capítulo.
13.1 O problema dos padrões
Para ilustrar a acção combinada de vários paradigmas, procurámos um exemplo não trivial que
envolvesse um tipo de dados multiforme (para exibir os aspectos organizacionais do paradigma orientado pelos objectos), e que beneficiasse da disponibilidade dos modos log, gen e lazy
(que suportam os paradigmas lógico e funcional).
Dentro destes limites pensámos em várias alternativas. Um problema possível seria o da
representação e manipulação das expressões da própria linguagem OM, incluindo a escrita
duma função de avaliação de expressões com retrocesso (esta seria uma versão sofisticada do
exemplo da secção 5.5.2.1). Outro problema interessante seria o da reconstrução em OM da
hierarquia Collection da linguagem Smalltalk, com a adição de métodos geradores às classes, a
246
OM – Uma linguagem de programação multiparadigma
introdução de versões lógicas em algumas estruturas, a criação duma classe de listas infinitas e
duma classe de grafos infinitos, etc.
Acabámos por nos decidir pelo seguinte problema de emparelhamento de padrões sobre cadeias de caracteres.
Problema 13.1-1 (Problema dos padrões) Capturar num sistema de classes a funcionalidade essencial das expressões do conhecido analisador lexical Lex [Lev92], enriquecendo,
no processo, essa funcionalidade com os seguintes três elementos adicionais: retrocesso, para
aumentar a capacidade de reconhecimento dos padrões; variáveis lógicas, para permitir criar
contextos dinâmicos dentro dos padrões; condições, para ser possível influenciar o processo de
emparelhamento, explorando por exemplo os valores correntes das variáveis lógicas.
Este tipo de funcionalidade geral (excluindo as variáveis lógicas) é suportado especificamente pela componente de emparelhamento de padrões da linguagem SNOBOL4 [Gri71], e
também, a um certo nível, pela linguagem Icon [Gri83]. Budd no seu livro [Bud95] apresenta
uma implementação fiel dos mecanismos do SNOBOL4 usando a sua linguagem Leda. O nos–
so exemplo irá diferir dos trabalhos referidos nos seguintes aspectos: na especificidade das primitivas do Lex; (2) no suporte para variáveis lógicas; (3) na linguagem usada na implementação.
13.1.1 Padrões
Começamos por introduzir as noções de padrão e emparelhamento de padrões no contexto do
caso específico das cadeias de caracteres:
• Um padrão é uma representação compacta de um conjunto de cadeias de caracteres;
• Emparelhamento de padrões é um mecanismo que determina se uma dada cadeia de
caracteres, ou um seu segmento, pertence ao conjunto representado por um dado padrão.
Os padrões genéricos que o nosso sistema de classes irá suportar são os que indicamos a seguir. A maioria deles tem tradução directa em Lex:
Forma do padrão
PChar#new(c)
PAny#new()
PStr#new(str)
PSet#new(cstr)
PNSet#new(cstr)
PSeq#new(p1,p2)
POr#new(p1,p2)
PZeroOrMore#new(p)
POneOrMore#new(p)
PBeg#new()
PEnd#new()
PTimes(p,n)
PUnify(p,v)
PCheck(p,b)
Lex
c
.
str
[cstr]
[~cstr]
p1p2
p1|p2
p*
p+
^
$
-
significado
representa o carácter c (conjunto singular)
representa todos os caracteres
representa a string str (conjunto singular)
conjunto dos caracteres contidos na string cstr
conjunto dos caracteres não contidos na string str
padrão p1 imediatamente seguido de p2
padrão p1 ou padrão p2
padrão p repetido zero ou mais vezes
padrão p repetido uma ou mais vezes
representa o início da linha
representa o fim da linha
padrão p repetido n vezes
igual ao padrão p, mas unifica resultado com var. v
igual ao padrão p se depois do emparelhamento, a exp.
b produzir true; senão não emparelha, e retrocede
13 Exemplo
247
Agora, exemplificamos alguns de padrões concretos e mostramos o seu significado:
PChar#new('a')
letra 'a'
POneOrMore#new(PChar#new('a'))
sequências não vazias de 'a's
PSeq#new(PBeg#new(),PEnd#new())
linha vazia
PSeq#new(PEnd#new(),PBeg#new())
linha vazia
POneOrMore#new(PSet#new("abcdefghijklmnopqrstuvwxyz"))
palavras não vazias constituídas por minúsculas
POneOrMore#new(PNSet#new(" ")) palavras não contendo qualquer espaço em branco
PSeq#new( PSeq#new(PChar#new('{'),PNSet#new('{}')), PChar#new('}')
cadeia delimitada por chavetas e sem chavetas no interior
Vejamos agora um exemplo envolvendo variáveis lógicas. O padrão twoEqual representa
dois caracteres consecutivos iguais. Para funcionar, a variável v tem de se encontrar livre no
momento em que o padrão é activado:
log Str v ;
Pattern twoEqual = PSeq#new( PUnify#new(PAny#new(),v), PUnify#new(PAny#new(),v) )
Finalmente um exemplo que envolve uma condição. O padrão wordInDict representa uma
palavra qualquer que se encontra num dicionário exterior ao padrão:
Pattern pal = POneOrMore#new(PSet#new("abcdefghijklmnopqrstuvwxyz"))
log Str v ;
Pattern wordInDict = PCheck#new(PUnify#new(pal,v), dict.belongs(v))
13.1.2 Uso dos padrões
No nosso programa, cada padrão será representado por um objecto com habilidade suficiente
para conseguir detectar numa linha de texto as cadeias por ele representadas. O emparelhamento efectua-se da esquerda para a direita, considerando-se, em casos de ambiguidade, sempre a sequência de caracteres mais longa em primeiro lugar. Os outros emparelhamentos possíveis podem ser gerados usando retrocesso.
Normalmente, activa-se um padrão enviando-lhe a mensagem match(line,matched). Na definição do método match, o parâmetro line é de entrada e representa a linha de texto com a qual
o padrão se tenta emparelhar; matched é um parâmetro do tipo cadeia de caracteres com modo
lógico e representa o resultado do emparelhamento. Consoante matched esteja ou não instanciado no momento da activação de match(line,matched), assim o método match funcionará em
modo de verificação ou em modo de descoberta (como é habitual nos programas em lógica).
Vejamos um exemplo no qual um padrão, twoEqual, é aplicado, usando match, a todas as linhas dum ficheiro de texto. Neste caso consideramos apenas o primeiro emparelhamento que é
possível efectuar dentro da cada linha:
class Main {
() #main() {
TextFile f = TextFile#Open("foo") ;
while( line = f.nextLine() )
{
log Str v, matched ;
Pattern twoEqual =
PSeq#new( PUnify#new(PAny#new(),v), PUnify#new(PAny#new(),v) )
;
if( twoEqual.match(line, matched) )
248
OM – Uma linguagem de programação multiparadigma
matched.printNl() ;
else
"///".printNl() ;
}
f.Close() ;
}
}
Vejamos um segundo exemplo no qual o mesmo padrão, twoEqual, é igualmente aplicado,
usando match, a todas as linhas dum ficheiro. Agora, a diferença reside no facto de considerarmos todos os emparelhamento que é possível efectuar dentro da cada linha.
class Main {
() #main() {
TextFile f = TextFile#Open("foo") ;
while( line = f.nextLine() )
{
log Str v, matched ;
Pattern twoEqual =
PSeq#new( PUnify#new(PAny#new(),v),PUnify#new(PAny#new(),v) ) ;
every( twoEqual.match(line, matched) )
matched.printNl() ;
"---".printNl() ;
}
f.Close() ;
}
}
13.1.3 A classe abstracta Pattern
Cada tipo de padrão é implementado numa classe distinta. Como é típico no paradigma orientado pelos objectos, a funcionalidade comum a todas as diferentes classes que implementam os
padrões é capturada numa superclasse partilhada. Evita-se assim redundância e consegue-se
dar um estatuto material à ideia abstracta de padrão. Chamamos Pattern a essa superclasse.
A classe Pattern é, portanto, uma classe abstracta. Como todas as classes abstractas, ela
ignora deliberadamente as variações dos aspectos concretos das suas subclasses, a ponto de
não haver interesse em criar instâncias directas suas: seriam entidades incompletas do ponto de
vista computacional, tendencialmente equivalentes ao termo divergeτ de F+ (cf. secção 2.3.3.1).
Apesar da superclasse Pattern ser abstracta, ao seu nível é já possível definir diversos métodos, nomeadamente o método público match, atrás referido, e dois novos métodos privados
auxiliares chamados matchRange e range. Apenas um novo terceiro método, matchAt, de quem
matchRange dependerá directamente, terá de ser definido de forma diferente em cada subclasse
concreta, pois é nesse método que serão codificadas as características específicas de cada forma de padrão particular.
Quando a mensagem matchAt(start,line) é enviada para um padrão, o receptor tenta emparelhar-se com algum segmento de line que comece no índice fixo start. Se conseguir, o método retorna o índice do carácter que se segue imediatamente ao texto emparelhado. Se não
conseguir, então o método falha. Existem padrões cuja natureza particular permite que possam
emparelhar de mais do que uma forma. Nesse caso, cada um desses padrões deve prever, no
13 Exemplo
249
respectivo método matchAt, as computações alternativas a serem activadas em caso de retrocesso (usando o operador de disjunção “|” e recursividade).
O método/gerador privado matchRange(line,start,end) tem a função de ir gerando sucessivamente todos os índices da linha line e de ir activando o método matchAt para tentar um
emparelhamento. Sempre que matchAt sucede, os parâmetros lógicos start e end são unificados
com os índices delimitadores do segmento de linha que emparelhou. O método matchRange
incorpora a seguinte pequena optimização: no caso do parâmetro start já vir instanciado,
matchRange usa directamente o valor de start na invocação de matchAt.
O método público match(line,matched) constitui essencialmente uma interface cómoda para
o método matchRange: ele limita-se a tentar unificar o parâmetro lógico matched com cada segmento que resultar do emparelhamento efectuado por matchRange.
class Pattern {
priv gen Int range(Int start, Int end) { // gera: start, start+1, …, end-1
start := start - 1 ;
return dowhile(repeat & start:=start+1, start < end) ;
}
// definição alternativa
// priv gen Int range(Int start, Int end) {
//
return start < end & (start | range(start + 1, end)
// }
priv gen Int matchAt(Int start, Str line) {
raise xAbstrAct ;
}
priv gen Int matchRange(Str line, log Int start, log Int end) {
Int i ;
if( start.isVar() )
return i := range(0, line.len()) // gera 0,1,2,…,(len-1)
& end == matchAt(i, line) & start == i & end
else
return end == matchAt(start, line)
}
gen Str match(Str line, log Str matched) {
log Int start, end ;
return matchRange(line, start, end)
& matched == line.slice(start, end-1) ;
}
}
13.1.4 As classes concretas
Todas as classes concretas definidas no programa têm a mesma estrutura geral: elas declaram
as variáveis de instância necessárias à representação de cada tipo de padrão; definem um construtor público chamado #new cuja aridade varia com o tipo de padrão; definem um método privado chamado matchAt cuja funcionalidade geral foi descrita na secção anterior.
A parte mais interessante das classes concretas é a forma como cada uma delas concretiza a
implementação do método básico de emparelhamento matchAt(start,line). A título de ilustração, vamos agora comentar a implementação de matchAt(start,line) nas classes PChar e
POneOrMore.
250
OM – Uma linguagem de programação multiparadigma
O método matchAt(start,line) da classe PChar só emparelha com sucesso se o índice start
indicar uma posição realmente dentro da linha line, e se na posição start se encontrar o carácter pretendido. Neste caso retorna start+1. Caso contrário falha.
O método matchAt(start,line) da classe POneOrMore pode emparelhar de múltiplas formas.
Quando activado pela primeira vez, o método procura uma sequência de emparelhamentos sucessivos do padrão interior p, o mais longa possível, Depois, em cada retrocesso, o método vai
emparelhando com sequências sucessivamente mais curtas de emparelhamentos de p até, finalmente, tentar um emparelhamento simples de p.
Para exemplificar uma utilização de POneOrMore considere o seguinte padrão, que representa
todos os advérbios de modo da Língua Portuguesa.
PSeq#new( POneOrMore#new(PSet#new(“abcdefghijklmnopqrstuvwxyz”)), PStr#new("mente"))
Note que quando este padrão é activado, perante um advérbio de modo bem formado, o seu
primeiro subpadrão começa por tentar sequências de caracteres excessivamente longas: tão
longas que capturam segmentos iniciais do sufixo “mente” e impedem qualquer reconhecimento com sucesso. Só à quinta tentativa, quando o primeiro subpadrão tenta finalmente uma
sequência que deixa de fora a subcadeia “mente”, é que o emparelhamento sucede.
Dadas estas explicações, apresentamos agora todas as classes concretas que integram o nosso programa:
class PChar : Pattern // Carácter fixo. lex:.
{
priv Char c;
SAMET #new(Char arg) {
{
SELFT s = #new() ; s.c := arg ; return s ; }
priv gen Int matchAt(Int start, Str line) {
return start < line.len()
& line[start] == c
& start + 1 ;
}
}
class PAny : Pattern // Qualquer carácter. lex:.
{
SAMET #new()
{
return SUPERC#new() ; }
priv gen Int matchAt(Int start, Str line) {
return start < line.len()
& start + 1 ;
}
}
class PStr : Pattern // Cadeia de caracteres fixa. lex:"..."
{
priv Str str ;
SAMET #new(Str arg)
{
SELFT s = #new() ; s.str := arg ; return s ; }
priv gen Int matchAt(Int start, Str line) {
Int end = start + str.len() ;
return str == line.slice(start, end-1) & end ;
}
}
class PSet : Pattern // Conjunto de caracteres. lex:[...]
{
priv Str chars ;
13 Exemplo
251
SAMET #new(Str arg)
{
SELFT s = #new() ; s.chars := arg ; return s ; }
priv gen Int matchAt(Int start, Str line) {
return start < line.len()
& chars.includes(line[start])
& start + 1 ;
}
}
class PNSet : Pattern // Conjunto complementar. lex:[~...]
{
priv Str chars ;
SAMET #new(Str arg)
{
SELFT s = #new() ; s.chars := arg ; return s ; }
priv gen Int matchAt(Int start, Str line) {
return start < line.len()
& !chars.includes(line[start])
& start + 1 ;
}
}
class PSeq : Pattern // Sequência de padrões. lex:P1P2
{
priv Pattern fir, sec ;
SAMET #new(Pattern f, Pattern x)
{
SELFT s = #new() ; s.fir := f ; s.sec := x ; return s ; }
priv gen Int matchAt(Int start, Str line) {
return sec.matchAt(fir.matchAt(start, line), line) ;
}
}
class POr : Pattern
// Padrões alternativos. lex:P1|P2
{
priv Pattern fir, sec ;
SAMET #new(Pattern f, Pattern x)
{
SELFT s = #new() ; s.fir := f ; s.sec := x ; return s ; }
priv gen Int matchAt(Int start, Str line) {
return fir.matchAt(start, line)
| sec.matchAt(start, line) ;
}
}
class PZeroOrMore : Pattern // Zero ou mais vezes. lex:p*
{
// Primeiro a sequência mais longa…
priv Pattern p ;
SAMET #new(Pattern arg)
{
SELFT s = #new() ; s.p := arg ; return s ; }
priv gen Int matchAt(Int start, Str line) {
return matchAt(p.matchAt(start, line), line)
| start ;
}
}
class POneOrMore : Pattern
// Uma ou mais vezes. lex:p+
{
// Primeiro a sequência mais longa…
priv Pattern p ;
SAMET #new(Pattern arg)
{
SELFT s = #new() ; s.p := arg ; return s ; }
priv gen Int matchAt(Int start, Str line) {
return matchAt(p.matchAt(start, line), line)
| p.matchAt(start, line) ;
}
}
class PBeg : Pattern // Início da linha. lex:^
{
SAMET #new()
{
return SUPERC#new() ; }
priv gen Int matchAt(Int start, Str line) {
return start == 0
& start ;
252
OM – Uma linguagem de programação multiparadigma
}
}
class PEnd : Pattern // Fim da linha. lex:$
{
SAMET #new()
{
return SUPERC#new() ; }
priv gen Int matchAt(Int start, Str line) {
return start == line.len()
& start ;
}
}
class PUnify : Pattern
// Se o emparelhamento suceder unifica o texto
{
// emparelhado com a variável lógica
priv Pattern p ;
priv log Str var ;
SAMET #new(Pattern arg, log Str x)
{
SELFT s = #new() ; s.p := arg ; s.var == x ; return s ; }
priv gen Int matchAt(Int start, Str line) {
Int end ;
return (end := p.matchAt(start, line))
& var == line.slice(start, end-1)
& end ;
}
}
class PCheck : Pattern
// Se o emparelhamento suceder verifcicsa a
{
// condição suplementar test
priv Pattern p ;
priv lazy Bool test ;
// Note que usamos aqui o modo lazy
SAMET #new(Pattern arg, lazy Bool x)
{
SELFT s = #new() ; s.p := arg ; s.line == x ; return s ; }
priv gen Int matchAt(Int start, Str line) {
Int end ;
return (end := p.matchAt(start, line))
& test
& end ;
}
}
class PTimes : Pattern
// n vezes
{
priv Pattern p ;
priv Int n ;
SAMET #new(Pattern arg, Int x)
{
SELFT s = #new() ; s.p := arg ; s.n == x ; return s ; }
priv gen Int matchAt(Int start, Str line) {
Int i, end ;
for( i:=1, end:=start ; i < n ; i:=i+1 )
if( end := p.matchAt(end, line) ) ;
else return fail ;
return end ;
}
}
Conclusões
Consideremos um projecto de programação constituído por múltiplas componentes heterogéneas, nomeadamente, um módulo de manipulação duma bases de dados, uma interface WWW,
um sistema pericial, etc. Num projecto com tão elevado grau de diversificação é frequente o
programador ver-se confrontado com a impossibilidade de expressar de forma directa algumas
das soluções que idealizou para algumas das componentes do projecto. A razão é a seguinte: a
linguagem de programação adoptada não cobre toda a gama de conceitos a que o programador
precisaria de recorrer para conseguir lidar de forma natural com uma gama tão diversificada de
problemas. As linguagens multiparadigma e as linguagens extensíveis têm um campo de aplicação privilegiado neste tipo de projectos, ao multiplicarem as ferramentas conceptuais que
colocam à disposição do programador.
Foi nesta linha de preocupações com a expressividade que concebemos a linguagem de programação multiparadigma OM. Nesta linguagem explorámos a ideia de usar um mecanismo
uniforme de extensão – o mecanismo dos modos – como veículo para a introdução de novos
conceitos e mecanismos na linguagem. Este mecanismo introduz homogeneidade na linguagem e evita que tenhamos de congelar à partida quais os paradigmas que a linguagem OM
deve suportar.
Constituiu nosso objectivo inicial o desenvolvimento dum mecanismo de extensão de base
estática, integrado no contexto duma linguagem orientada pelos objectos estaticamente tipificada. Assim, o modelo semântico que elaborámos, para a linguagem OM, foi um modelo
tipificado.
O mecanismo dos modos, sendo relativamente simples, encerra um apreciável potencial,
como o demonstram os diversos modos que definimos na biblioteca padrão da linguagem.
Quanto às limitações deste mecanismo, existem duas que importa referir: é um mecanismo
síncrono, logo não adaptado à expressão de concorrência (é possível definir um modo de corotinas, mas não mais do que isso); a natureza do mecanismo não permite que ele seja usado em
domínios de valores específicos (por exemplo, não se consegue criar uma sublinguagem de
restrições sobre números reais mediante a simples introdução dum modo).
A maior parte do nosso trabalho, em termos de volume e esforço, foi dedicada ao desenvolvimento em paralelo da linguagem e do seu modelo tipificado. Este tipo de desenvolvimento
em paralelo é sempre recomendável, na medida em que o modelo ajuda a compreender a linguagem e permite tomar decisões fundamentadas quanto à sua evolução. No culminar deste
254
OM – Uma linguagem de programação multiparadigma
processo, tivemos a satisfação de verificar a naturalidade com que foi possível fazer surgir o
mecanismo dos modos através de mais uma extensão do nosso modelo modelo.
O modelo semântico que desenvolvemos para a linguagem OM integra muito do saber-fazer acumulado ao longo da última década no campo dos modelos tipificados para linguagens de objectos e classes. As principais virtualidades do nosso modelo são as seguintes: ele
cobre de forma organizada um largo espectro de mecanismos; é razoavelmente abordável;
suporta variantes melhoradas de certos mecanismos (por exemplo, a técnica de ocultação da
informação que facilita a inicialização de objectos (cf. capítulo 7)); considera uma solução
específica para o conhecido problema do conflito entre um mecanismo de herança geral e a
relação de subtipo (cf. capítulo 5).
Em dois pontos desta dissertação fomos obrigados a lidar com problemas de indecidibilidade: primeiro, no cálculo-lambda polimórfico F+ que usámos na escrita do modelo como
veículo de expressão semântica (cf. secção 2.3.4.5); depois, no sistema de coerções extensível
que introduzimos no capítulo 10.
Consideramos que a linguagem OM já atingiu uma forma estável, exceptuando possivelmente os mecanismos descritos no capítulo 11, os quais continuamos a tentar fazer evoluir no
sentido duma ainda maior simplificação.
O principal ponto que ficou em aberto, e que irá requerer trabalho adicional, foi o problema
identificado no final da secção 11.6: o problema da inferência de argumentos de instanciação
de entidades paramétricas em presença dum sistema de coerções.
Outro ponto em aberto consiste na questão do desenvolvimento duma implementação para
a linguagem OM. Para a realização desta tarefa, tencionamos usar a conhecida técnica de bootstrapping (descrita em [ASU85], por exemplo) pois gostaríamos de exercitar a escrita do compilador de OM usando a própria linguagem OM. Pretendemos codificar as equações semânticas de OM em CAML como ponto de partida do processo de bootstrapping, não obstante este
método nos obrigar, numa primeira fase, a tratar OM como uma linguagem não-tipificada –
note que o CAML é um sistema de segunda ordem, logo menos poderoso do que o sistema de
ordem superior F+ usado originalmente para definir a linguagem OM.
Bibliografia
[AC93]
R.Amadio, L.Cardelli. Subtyping recursive types. ACM Transactions on Programming Languages and Systems, 15(4):575-631, 1993.
[AC94]
M.Abadi, L.Cardelli. A semantics of object types. Proc. Ninth Annual IEEE Symposium on
Logic in Computer Science, Paris, France, 1994.
[AC96a]
M.Abadi, L.Cardelli. A Theory of Objects. Springer, 1996.
[AC96b]
M.Abadi, L.Cardelli. On subtyping and matching. ACM Transactions on Programming Languages and Systems, 18(4):401-423, 1996.
[ACC93]
M.Abadi, L.Cardelli, P.Curien. Formal Parametric Polymorphism. Proc. 20th Annual ACM
Symposium on Principles of Programming Languages, 1993.
[ACPP91]
M.Abadi, L.Cardelli, B.Pierce, G.Plotkin. Dynamic typing in a statically typed language.
ACM Transactions on Programming Languages and Systems, 13(2):237-268, 1991.
[ACPR92]
M.Abadi, L.Cardelli, B.Pierce, D.Rémy. Dynamic typing in polymorphic languages. Proc.
SIGPLAN Workshop on ML and its Applications, 1992.
[ACV96]
M.Abadi, L.Cardelli, R.Viswanathan. An interpretation of objects and object types. Conference Record of POPL'96: The 23rd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, 1996.
[AF96]
M.Abadi, M.Fiore. Syntactic Considerations on recursive types. Proceedings of the Eleventh
Annual IEEE Symposium on Logic in Computer Science. págs. 242-252. IEEE Computer
Society, 1996.
[AG98]
K.Arnold, J.Gosling. The Java Programming Language. Addison-Wesley, Reading, MA, 2nd
edition, 1998.
[Aït90]
H.Aït-Kaci. The WAM: A (Real) Tutorial, Digital, Paris Research Laboratory, 1990.
[Ama91]
R.Amadio. Recursion over realisable structures. Information and Computation. 91(1):55-86,
1991.
[AP90]
M.Abadi, G.Plotkin. A PER model of polymorphism and recursive types. Proc. IEEE Symposium on Logic in Computer Science, págs. 355-365, 1990.
[ASU85]
A.Aho, R.Sethi, J.Ullman. Compilers: Principles, Techniques, and Tools. Addison-Wesley
Pub Co, 1985.
[Bar84]
H.Barendret. The Lambda Calculus. Its Syntax and Semantics. North-Holland, 1984.
[BB85]
C.Böhm, A.Berarducci. Automatic synthesis of typed λ-programs on term algebras. Theoretical Computer Science, 39, págs. 135-154, 1985.
[BCC+96]
K.Bruce, L.Cardelli, G.Castagna. The Hopkins Objects Group, G.Leavens, B.Pierce. On binary
methods. Theory and Practice of Object Systems, 1(3):221-242, 1996.
[BCD+93]
K.Bruce, J.Crabtree, A.Dimock, R.Muller, T.Murtagh, R. van Gent. Safe and decidable type
checking in an object-oriented language. In Proc. ACM Symp. on ACM Conf. on Object-Oriented Programming: Systems, Languages and Applications. págs. 29-46, 1993.
[BCK94]
K.Bruce, J.Crabtree, G.Kanapathy. An operational semantics for TOOPLE: A statically-typed object-oriented programming language. Em Mathematical Foundations of Programming Semantics. págs. 603-626. LNCS 802. Springer-Verlag, 1994.
[BCP99]
K.Bruce, L.Cardelli, B.Pierce. Comparing Object Encodings. Information and Computation,
1999.
256
OM – Uma linguagem de programação multiparadigma
[BFSG98]
K.Bruce, A.Fiech, A.Schuett, R.Gent. PolyTOIL: A type-safe polymorphic object-oriented
language. Versão actualizada de [BSG95]. Williams College, 1998.
[BHJL86]
A.Black, N.Hutchinson, E.Jul, H.Levy. Object structure in the Emerald system. In Proc. ACM
Symp. on ACM Conf. on Object-Oriented Programming: Systems, Languages and Applications.
págs. 78-86, 1986.
[BL90]
K.Bruce, G.Longo. A modest model of records, inheritance and bounded quantification. Information and Computation. 87(1/2):196-240, 1990.
[BM92]
K.Bruce, J.Mitchell. PER models of subtyping, recursive types and higher-order polymorphism. Proc. ACM Symp. on Principles of Programming Languages. págs. 316-327, 1992.
[BPF97]
K.Bruce, L.Petersen, A.Fiech. Subtyping is not a good “Match” for object-oriented languages”. ECOOP '97 Proceedings, LNCS 1241, Springer-Verlag, págs. 104-127, 1997.
[Bru94]
K.Bruce. Safe type checking in a statically-typed object-oriented programming language. 20
Proc. ACM Symp. on Principles of Programming Languages. págs. 285-298, 1994.
[BSG95]
K.Bruce, A.Schuett, R.Gent. PolyTOIL: A type-safe polymorphic object-oriented language.
Em Proc. 9th European Conference on Object Oriented Programming. págs. 26-51. Aarhus, Dinamarca, 1995.
[Bud95]
T.Budd. Multiparadigm Programming in Leda. Addison Wesley Longman, 1995.
[Car86]
L. Cardelli. Amber. Em Combinators and Functional Programming Languages. LNCS 242.
Sprin0.ger Verlag, 1986.
[Car88]
L. Cardelli. A semantics of multiple inheritance. Information and Computation 76:138-164.
1988. Versão anterior em: Semantics of Data Types, LNCS 173, págs. 51-67. Springer Verlag.
1984, 1988.
[Car88]
L. Cardelli. Structural subtyping and the notion of power type. 23rd ACM Symposium on
Principles of Programming Languages, 1988.
[Car89]
F.Cardone. Relational semantics for recursive types and bounded quantification. ICALP,
Berlin. LNCS 372:164-178. Springer, 1989.
[Car90]
L. Cardelli. Notes about F . Notas não publicadas disponíveis na página WWW do autor, 1990.
[Car94]
L.Cardelli. Extensible records in a pure calculus of subtyping. C.A. Gunter and J.C. Mitchell,
Editors. Theoretical Aspects of Object-Oriented Programming. MIT Press. págs. 373-425, 1994.
[Car97]
L.Cardelli. Type Systems. Em Handbook of Computer Science and Engineering, chapter 103.
CRC Press, 1997.
[Cas96]
G.Castagna. Integration of parametric and “ad hoc” second order polymorphism in a calculus with subtyping. Formal Aspects of Computing, 8(3):247-293, 1996.
[Cas98]
G.Castagna. Foundations of Object-oriented Programming. Tutorial. ETAPS’98, Lisboa,
1998.
[CCH+89]
P.Canning, W.Cook, W.Hill, J.Mitchell, W.Olthoff. F-bounded polymorphism for object-oriented programming. Proc. of. Conf. on Functional Programming Languages and Computer
Architecture, 1989.
[CDJ+89]
L. Cardelli, J. Donahue, M. Jordan, B. Kaslow, G. Nelson. The Modula-3 type system. Conf.
Rec. ACM Symp. on Principles of Programming Languages. págs. 202-212, 1989.
[CG92]
P.Curien,G.Ghelli. Coherence of subsumption, minimum typing and the type checking in F≤.
Mathematical Structures in Computer Science, 2(1), 1992.
[CGL92]
G.Castagna, G.Gelli, G.Longo. A calculus for overloaded functions with subtyping. ACM
Conference on LISP and Functional Programming. págs. 182-192, 1992.
[CHC90]
W.Cook, W.Hill, P.Canning. Inheritance is not subtyping. Proc. ACM Symp. on Principles of
Programming Languages, 1990.
ω
≤
Bibliografia
257
[Chu36]
A.Church. An Unsolvable Problem of Elementary Number Theory. American Journal of Mathematics. 58:345-363, 1936.
[Chu40]
A.Church. A formulation of the simple theory of types. Journal of symbolic Logic. 5:56-68,
1940.
[CM81]
W.Clocksin, C.Mellish. Programming in Prolog. Springer-Verlag, Berlin, 1981.
[CM91]
L.Cardelli, J.Mitchell. Operations on records. Mathematical Structures in Computer Science,
1(1):3-48. 1991. In Theoretical Aspects of Object-Oriented Programming, MIT Press. 1994,
1991.
[CMMS94]
L.Cardelli, J.Mitchell, S.Martini, A.Scedrov. An extension of system F with subtyping. Information and Computation, 109(1/2):4-56, 1994.
[Coo89]
W.Cook. A proposal for making Eiffel type-safe. ECOOP '89 Proceedings. págs. 57-72, 1989.
[Cop85]
M.Coppo. A completeness theorem for recursively-defined types. Proc. ICALP, Berlin. LNCS
194. Springer, 1985.
[CP89]
W.Cook, J.Palsberg. A denotational semantics of inheritance and its correctness. Proc. ACM
Conf. on Object-Oriented Programming: Systems, Languages and Applications, 1989.
[CP94]
G.Castagna, B.Pierce. Decidable Bounded Quantification. 21st Annual Symposium on Principles of Programming Languages. Portland, 1994.
[CW85]
L.Cardelli, P.Wegner. On understanding types, data abstraction and polymorphism. ACM
Computing Surveys 17(4):471-522, 1985.
[dB72]
N. de Bruijn. Lambda-calculus notation with nameless dummies. Indag. Math. 34(5):381-392,
1972.
[DFP86]
NJ.Darlington, A.Field. H.Pull. The Unification of Functional and Logic Languages. Em
"Logic Programming: Functions, Relations and Equations". D.DeRoot, G.Lindstrom (editors).
Prentice-Hall, 1986.
[DG87]
L.DeMichiel, R.Gabriel. Common Lisp Object System overview. ECOOP '87 Proceedings.
págs. 151-170. Springer, 1987.
[Dia90]
A.M.Dias. Implementação de um sistema de programação em Lógica Contextual. Relatório
técnico 4.91. Dep. Informática, Fac. de Ciências e Tecnologia, Univ. Nova de Lisboa, 1990.
[ES90]
M.Ellis, B.Stroustrup. The Annotated C++ Reference Manual. Addison-Wesley, 1990.
[ESTZ94]
J.Eifrig, S.Smith, V.Trifonov, A.Zwarico. Application of OOP Type Theory: State, Decidability, Integration. Proc. OOPSLA 94. págs. 16-30, 1994.
[Fel90]
M.Felleisen. On the Expressive Power of Programming Languages, 1990.
[FHM94]
K.Fisher, F.Honsell, J.Mitchell. A lambda calculus of objects and method specialisation. Nordic J. Computing. 1:3-37, 1994.
[Flo79]
R.Floyd. The Paradigms of Programming. Communications of the ACM, 22(8):455-460,
1979.
[FM95]
K.Fisher, J.Mitchell. The Development of Type Systems for Object-Oriented Languages.
Theory and Practice of Object Systems, 1(3), págs. 189-220, 1995.
[FM96]
K.Fisher, J.Mitchell. Classes = Objects + Data Abstraction, 1996.
[FR99]
K.Fisher, FJ.Reppy. The design of a class mechanism for Moby. Proc. SIGPLAN Conference
on Programming Language Design and Implementation. Atlanta. Georgia, 1999.
[Gab85]
J.Gabriel, T.Lindholm, E.Lusk, R.Overbeek. A Tutorial on the Warren Abstract Machine. Argonne National Laboratory, 1985.
[Ghe93]
G.Ghelli. Recursive types are not conservative over F≤. TLCA’93, International Conference
on Typed Lambda Calculi and Applications, LNCS 664:146-162. Springer, 1993.
258
OM – Uma linguagem de programação multiparadigma
[Gir71]
J.Girard. Une extension de l’interprétation de Gödel à l’analyse, e son application à l’élimination des coupures dans l’analyse el la théorie des types. Proceedings of the Second Scandinavian Logic Symposium, J.Fenstad ed. págs. 63-92. North-Holland, 1971.
[Gir72]
J.Girard. Interprétation functionelle el élimination des coupures de l’arithmétique d’ordre
supérieus. PhD thesis. Université Paris VII, 1972.
[GM83]
A.Goldberg, D.Robson. Smalltalk-80: The Language and its Implementation. Addison-Wesley, 1983.
[Goo81]
J.Goodwin. Why Programming Environments Needs Dynamic Data Types. IEEE Transactions on Software Engineering, val.SE-7. No.5, 1981.
[GP96]
G.Ghelli, B.Pierce. Bounded Existentials and Minimal Typing. Relatório preliminar, 1996.
[Gri71]
R.Griswold, J.Poage, I.Polonsky. The SNOBOL4 Programming Language. 2ª edição. Englewood Cliffs, N.J. Prentice-Hall Inc, 1971.
[Gri83]
R.Griswold, M.Griswold. The Icon Programming Language. Prentice Hall, 1983.
[Gun92]
C.Gunter. Semantics of Programming Languages: Structures and Techniques. The MIT
Press, 1992.
[Har89]
S.Haridi, S.Janson, J.Montélius, P.Brand, B.Hausman. The Andorra Project. Swedish Institute
of Computer Science. Research project proposal, 1989.
[Her89]
M.Hermenegildo. High-Performance Prolog Implementation: the WAM and Beyond. Tutorial. 6º ICLP. Lisboa. Portugal, 1989.
[Hen92]
F.Henglein. Dynamic typing. European Symposium on Programming. Rennes. França, 1992.
[Hog84]
C.Hogger. Introduction to Logic Programming. Academic Press, 1984.
[HP95]
M.Hofmann, B.Pierce. Positive subtyping. Proc. 22th Annual ACM Symposium on Principles
of Programming Languages, 1995.
[HU79]
J.Hopcroft, J.Ullman. Introduction to Automata Theory, Languages, and Computation.
Addison-Wesley, 1979.
[JG86]
M.Jenkins, J.Glasgow. Programming Styles in Nial. IEEE Software. Janeiro, 1986.
[KB85]
F.Kluzniak, J.Bien. Prolog for Programmers. Academic Press, 1985.
[KR78]
B.Kernighan, D.Ritchie. The C Programming Language. Prentice-Hall, 1978.
[Kra83]
G.Krasner. Smalltalk-80, Bits of History, Words of Advice. Addison Wesley, 1983.
[KRB91]
G.Kiczales, des Rivières, D.Bobrow. The Art of the Metaobject Protocol. The MIT Press,
1991.
[KMMN87] B.Kristensen, O.Madsen, B.Moller-Pedersen, K.Nygaard. The Beta Programming Language.
Em Research Directions in Object-Oriented Programming. págs. 7-48. MIT Press. Cambridge,
MA, 1987.
[KR93]
S.Kamin, U.Reddy. Two Semantic Models of Object-Oriented Languages. Em C.Gunter,
J.Mitchell. Theoretical Aspects of Object-Oriented Programming: Types, Semantics, and Language Design. The MIT Press, 1993.
[Lan65]
P.Landin. A Correspondence Between Algol 60 and Church’s Lambda-Notation. Communications of the ACM, 8, 1965.
[Ler96]
X.Leroy. The Caml Light system, release 0.71 (reference manual). INRIA, França, 1996.
[Lev92]
J.Levine, et al. Lex & Yacc. O'Reilly & Associates, 1992.
[LP91]
W.LaLonde, J.Pugh. Subclassing ≠ subtyping ≠ is-a. Journal of Object-Oriented Programming,
págs. 57-62, 1991.
[LRVD99]
X.Leroy, D.Rémy, J.Vouillon, D.Doligez. The Objective Caml system release 2.04. INRIA,
1999.
Bibliografia
259
[LSA77]
B.Liskov, A.Snyder, R.Atkinson, C.Schaffert. Abstraction Mechanisms in CLU. Comm. ACM
20:564-576, 1977.
[Mac62]
J.MacCarty. Lisp 1.5 Programmer's Manual. MIT Press. Cambridge. Mass, 1962.
[Mau95]
M.Mauny. Functional programming using Caml Light (tutorial). INRIA. França, 1995.
[Mey88]
B.Meyer. Object-Oriented Software Construction. Prentice-Hall, 1988.
[Mey92]
B.Meyer. Eiffel: the Language. Prentice-Hall, 1992.
[MH83]
C.Mellish, S.Hardy. Integrating Prolog Into the Poplog Environment. IJCAI 1983. Karlsruhe.
West Germany, 1983.
[Mit90]
J.Mitchell. Toward a typed foundation for method specialisation and inheritance. Proc. 17th
ACM Symposium on Programming Language. págs. 109-124, 1990.
[Mor73]
Morris. Types are not Sets. First ACM Symposium on Principles of Programming Languages.
págs. 120-124, 1973.
[MP85]
J.Mitchell, G.Plotkin. Abstract types have existential type. Proc. 12th Annual ACM Symposium on Principles of Programming Languages, 1985.
[MR91]
Q.Ma, J.Reynolds. Types, abstraction, and parametric polymorphism, part 2. Proc. Mathematical foundations of Programming Semantics. Springer-Verlag, 1991.
[MTH90]
R.Milner, M,Tofte, R.Harper. The Definition of Standard ML. MIT Press, 1990.
[Omo92]
S.Omohundro: The Sather 1.0 Specification. International Computer Science Institute, Berkeley, 1992.
[OW97]
M.Odersky, P.Wadler: Pizza into Java: Translating theory into practice. 24th ACM Symposium on Programming Languages, Paris, França, 1997.
[PAC94]
G.Plotkin, M.Abadi, L.Cardelli. Subtyping and parametricity. Proceedings, Ninth Annual
IEEE Symposium on Logic in Computer, Paris, France, 1994.
[Pas86]
G.Pascoe. Encapsulators: A New Software Paradigm in Smalltalk-80. OOPSLA’86 Proceedings. Portland, 1986.
[Pie93]
B.Pierce. Simple type-theoretic foundations for object-oriented programming. Jounal of
Functional Programming, 4(2):207-248, 1993.
[Pie93b]
B.Pierce. Mutable Objects. “Working draft” disponível no sítio da Internet do autor, 1993.
[Pla91]
J.Placer. Multiparadigm Research: A new direction in language design. ACM SIGPLAN
Notices, 26:3. págs. 9-17, 1991.
[Plo81]
G.Plotkin. A Structural Approach to Operational Semantics. DAIMI Technical Report
FN-19. Computer Science Department. Aarhus University, 1981.
[PPT87]
A.Porto, L.M.Pereira, L.Trindade. UNL Prolog User’s Guide Manual. Departamento de Informática, Universidade Nova de Lisboa, 1987.
[PT93]
B.Pierce, D.Turner. Statically Typed Friendly Functions via Partially Abstract Types. Relatório técnico ECS-LFCS-93-256, Universidade de Edinburgo, 1993.
[PT94]
B.Pierce, D.Turner. Simple type-theoretic foundations for object-oriented programming.
Jounal of Functional Programming, 4(2):207-248, 1994.
[Rém89]
D.Rémy. Typechecking records and variants in a natural extension of ML. Conf. Rec. ACM
Symp. on Principles of Programming Languages, 1989.
[Rém92]
D.Rémy. Typing record concatenation for free. 19th ACM Symp. on Principles of Programming Languages. págs. 166-176, 1992.
[Rèv85]
G.Rèvész. Introduction to Formal Languages. McGraw-Hill, 1985.
[Rey74]
J.Reynolds. Towards a theory of type structure. in Colloquium sur la programmation. págs.
408-423. LNCS 19. Springer-Verlag, 1974.
260
OM – Uma linguagem de programação multiparadigma
[Rey78]
J.Reynolds. User Defined Types and Procedural Data Structures as Complementary
Approaches to Data Abstraction. reimpressão em C.A. Gunter and J.C. Mitchell, Editors.
Theoretical Aspects of Object-Oriented Programming. MIT Press, 1978.
[Rey80]
J.Reynolds. Using Category Theory to Design Implicit Conversions and Generic Operators.
In Theoretical Aspects of Object-Oriented Programming, MIT Press, 1980.
[Rey83]
J.Reynolds. Types, abstraction, and parametric polymorphism. in Information Processing.
R.Mason ed. North Holland. págs. 513-523, 1983.
[RV98]
D.Rémy, J.Vouillon. Objective ML: An effective object-oriented extension to ML. TAPOS, 4,
págs. 27-50, 1998.
[SCB+86]
C. Schaffert, T. Cooper, B. Bullis, M. Kilian, C. Wilpolt. An introduction to Trellis/Owl. Proc.
ACM Conf. on Object-Oriented Programming: Systems, Languages and Applications, 1986.
[Sch86]
D. Schmidt. Denotational Semantics. W.C.Brown. Dubuque. Iowa, 1986.
[Sch94]
D. Schmidt. The Structure of Typed Programming Languages. Foundations of Computing
Series. The MIT Press, 1994.
[Sco73]
D.Scott. Models for Various Type-free Calculi. Em Logic, Methodology and Philosophy of
Science IV. North-Holland. Amsterdam, 1973.
[Seb93]
R.Sebesta. Concepts of Programming Languages. The Benjamin/Cummings Publishing Company, Inc, 1993.
[Sha94]
D.Shang. Covariant Specification. ACM SIGPLAN Notices. 29:12. págs. 58-65, 1994.
[Sha95]
D.Shang. Covariant Deep Subtyping Reconsidered. ACM SIGPLAN Notices. 30(5):21-28,
1995.
[SF94]
A. Sabry, J.Field. Reasoning about Explicit and Implicit Representations of State. ACM
SIGPLAN Workshop on STATE in Programming Languages, 1994.
[Sit]
D.Sitaram. Programming in Schelog. http://cs-tr.cs.rice.edu/~dorai/.
[Smi83]
MB.Smith. Reflexion and Semantics in Lisp. Proceedings of the 1984 ACM Principles of Programming LanguagesConference, 1983.
[SP94]
M.Steffen, B.Pierce. Higher-Order Subtyping. IFIP Working Conference on Programming
Concepts, Methods and Calculi (PROCOMET), 1994.
[SS86]
L.Sterling. E.Shapiro. The Art of Prolog. MIT Press, 1986.
[Ste84]
G.Steele: Common List: The language. Digital Press, 1984.
[Sto77]
J.Stoy: Denotational Semantics: The Scott-Strachey Approach to Programming Language
Theory. The MIT Press, 1977.
[Str67]
C. Strachey. Fundamental concepts in programming languages. Lecture notes for the International Summer School in Computer Programming. Copenhagen, 1967.
[Sun 95]
Sun Microsystems. The Java™ Language specification, version 1.0Beta. Sun Microsystems,
1995.
[Wad89]
P.Wadler. Theorems for free! Proc. 4th International Symposium on Functional Programming
Languages and Computer Architecture. Springer-Verlag, 1989.
[Wan87]
M.Wand. Complete type inference for simple objects. Proc. 2nd Annual IEEE Symposium on
Logic in Computer Science. 19. Springer-Verlag, 1987.
[Wan89]
M.Wand. Type inference for record concatenation and multiple inheritance. Proc. IEEE
Symposium on Logic in Computer Science. págs. 92-97, 1989.
[War77]
D.Warren. Implementing Prolog: compiling predicate logic programs, DAI Report 39-40.
University of Edinburgh, 1977.
[War83]
D.Warren. An Abstract Prolog Instruction Set. SRI Technical Note 309. SRI International,
1983.
Bibliografia
261
[War88]
D.Warren. Implementation of Prolog, Lecture notes, Tutorial nº 3, Symp. on Logic Programming. Seattle. USA, 1988.
[Wei95]
MM.Weiss. Data Structures and Algorithm Analysis. Benjamin Cumminngs Publishing Company, 1995.
[Win93]
G.Winskel. The Formal Semantics of Programming Languages: An Introduction. MIT
Press, 1993.