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.