• Nenhum resultado encontrado

Integração de linguagens funcionais à plataforma .NET utilizando o framework Phoenix

N/A
N/A
Protected

Academic year: 2021

Share "Integração de linguagens funcionais à plataforma .NET utilizando o framework Phoenix"

Copied!
123
0
0

Texto

(1)Pós-Graduação em Ciência da Computação. Integração de Linguagens Funcionais à Plataforma .NET utilizando o Framework Phoenix Por. Guilherme Amaral Avelino  . Dissertação de Mestrado. Universidade Federal de Pernambuco [email protected] www.cin.ufpe.br/~posgraduacao. RECIFE, AGOSTO/2008.

(2) UNIVERSIDADE FEDERAL DE PERNAMBUCO CENTRO DE INFORMÁTICA PÓS-GRADUAÇÃO EM CIÊNCIA DA COMPUTAÇÃO. Guilherme Amaral Avelino. Integração de Linguagens Funcionais à Plataforma .NET Utilizando o Framework Phoenix. ESTE TRABALHO FOI APRESENTADO À PÓS-GRADUAÇÃO EM CIÊNCIA DA COMPUTAÇÃO DO CENTRO DE INFORMÁTICA DA UNIVERSIDADE FEDERAL DE PERNAMBUCO COMO REQUISITO PARCIAL PARA OBTENÇÃO DO GRAU DE MESTRE EM CIÊNCIA DA COMPUTAÇÃO.. ORIENTADOR: Prof. Dr. ANDRÉ LUIS DE MEDEIROS SANTOS. RECIFE, AGOSTO/2008.

(3)  . Avelino, Guilherme Amaral Integração de linguagens funcionais à plataforma .NET utilizando o framework Phoenix / Guilherme Amaral Avelino. – Recife: O Autor, 2008. 104 folhas : il., fig., tab. Dissertação (mestrado) – Universidade Federal de Pernambuco. CIn. Ciência da computação, 2008. Inclui bibliografia e apêndices. Linguagem de programação. 2. Compiladores. I. Título. 005.1.  . CDD (22.ed.). MEI2008-100.

(4) AGRADECIMENTOS  Agradeço a todos aqueles que, direta ou indiretamente contribuíram para a realização desta pesquisa e em especial: • Primeiramente a Deus, por ter me dado saúde, inteligência e perseverança necessária à execução deste projeto. • Aos meus pais, Paulo Lustosa Avelino e Aldênia Maria Amaral Santos Avelino, pelo carinho, amor e dedicação com que se empenharam na minha formação pessoal e profissional; • A Lyvia Basílio Caland, minha namorada, pela compreensão nos momentos de ausência e pelo apoio e incentivo constante durante esta fase de minha vida; • Ao professor André Santos, pela oportunidade de desenvolver este projeto e acima de tudo por sua excelente orientação e auxílio nos mais diversos problemas enfrentados durante a realização deste; • Aos amigos do mestrado, em especial a Armando Soares, Vinícius Pádua e Marcos Duarte, pela motivação, auxílio e companheirismo. Além de um convívio. fraterno. que. proporcionou. um. ambiente. propício. ao. desenvolvimento deste trabalho; • A Simon Peyton Jones, Tim Chevalier e demais participantes do fórum do GHC que contribuíram com informações importantes sobre o GHC e a linguagem CORE; • A Andy Ayers e Matt Mitchell, membros da equipe desenvolvimento do Phoenix, pela sempre atenciosa forma com que responderam as minhas mais variadas dúvidas sobre o uso desta ferramenta. • A Monique Louise de Barros Monteiro, pelas explicações a respeito do projeto Haskell .NET e pelas dicas e comentários bastante úteis para o desenvolvimento deste projeto. • À Microsoft Research pelo apoio financeiro, permitindo que eu me dedicasse integralmente ao projeto..

(5) • Ao Centro de Informática e a sua excelente equipe de professores e profissionais,. que. muito. contribuíram. para. minha. formação. proporcionaram a base para o desenvolvimento deste trabalho. • A todos os meus amigos e familiares, pelo apoio.. e.

(6) RESUMO  Linguagens funcionais se destacam pelo seu alto poder de expressão e abstração,. promovido. por. construções. de. alto. nível. como. polimorfismo. paramétrico, funções de alto nível e aplicações parciais. Embora estes recursos sejam bastante úteis, tradicionalmente, linguagens funcionais têm sido pouco empregadas fora do ambiente acadêmico. Esta situação é em parte explicada pela ausência de uma infra-estrutura de desenvolvimento que forneça ferramentas e APIs capazes de aumentar a produtividade e permita o uso das mais recentes tecnologias. Uma alternativa para fornecer esta infra-estrutura é integrar linguagens funcionais a plataformas que disponibilizem tais facilidades, como a .NET. Embora a plataforma .NET tenha sido projetada de forma a suportar múltiplas linguagens, seu foco foi dado ao suporte dos paradigmas imperativo e orientado a objeto, carecendo de estruturas que permitam um mapeamento direto de linguagens funcionais. Objetivando. estudar. novas. técnicas. de. mapeamento. de. estruturas. funcionais na plataforma .NET, neste trabalho foi desenvolvido um compilador funcional que gera código .NET, utilizando o framework Phoenix. O uso do framework Phoenix além de auxiliar na geração inicial do código permitiu que análises e otimizações fossem feitas, posteriormente, melhorando o desempenho dos programas gerados. Palavras-chave: Linguagem funcional; NET; Phoenix; STG; Compiladores..

(7) ABSTRACT  Functional languages stand out for their high power of expression and abstraction, promoted by high-level buildings as parametric polymorphism, highlevel functions and partial applications. However these features are quite useful, traditionally, functional languages have been little used outside the academic environment. This is partly explained by the lack of a development infrastructure that provides tools and APIs which are capable of increasing the productivity and allow the use of latest technologies. An alternative to provide this infrastructure is to integrate functional languages to platforms that provide such facilities, such as .NET. Although the platform. NET has been designed in a way that supports multiple languages, its focus was given to the support of imperative paradigms and the object oriented, lack of structures that allow a direct mapping of functional languages. Aiming to study new techniques for mapping of functional structures on the platform. NET, in this work, a functional compiler that generates .NET code was developed, using Phoenix framework. Apart from helping in generating initial code, the use of the Phoenix framework permitted analyses and optimizations to be made, subsequently, improving the performance of the generated programs. Keywords: Functional language; NET; Phoenix; STG; Compilers..

(8) SUMÁRIO  1. INTRODUÇÃO................................................................................................................. 16. 1.1 CONTEXTO E MOTIVAÇÃO ............................................................................................. 16 1.2 ORGANIZAÇÃO DA DISSERTAÇÃO .................................................................................. 18 2. PROGRAMAÇÃO FUNCIONAL NA PLATAFORMA .NET................................................ 21. 2.1 INTRODUÇÃO A LINGUAGENS FUNCIONAIS ...................................................................... 21 2.1.1 Funções de alta ordem ........................................................................... 22 2.1.2 Aplicação parcial de funções................................................................ 22 2.1.3 Avaliação preguiçosa ............................................................................. 23 2.1.4 Polimorfismo paramétrico........................................................................ 24 2.1.5 Tipos algébricos......................................................................................... 25 2.2 PLATAFORMA .NET........................................................................................................ 26 2.2.1 CLR.............................................................................................................. 26 2.2.2 Outras Implementações da CLI.............................................................. 28 2.3 INTEGRAÇÃO À PLATAFORMA .NET ................................................................................ 29 2.3.1 Bridge ......................................................................................................... 29 2.3.2 Compilação .............................................................................................. 30 2.3.3 Estendendo a CLI...................................................................................... 31 2.4 MAPEANDO ESTRUTURAS FUNCIONAIS EM AMBIENTES OO ............................................... 32 2.4.1 Closures ...................................................................................................... 32 2.4.1.1 Projetando uma closure .................................................................................................34. 2.4.2 Mecanismo de aplicação de funções.................................................. 36 2.4.2.1 Modelo push/enter..........................................................................................................37 2.4.2.2 Modelo eval/apply .........................................................................................................37. 2.4.3 Representação de tipos algébricos....................................................... 38 2.5 IMPLEMENTAÇÕES EXISTENTES ......................................................................................... 39 2.5.1 Hugs for .NET .............................................................................................. 40 2.5.2 Mondrian.................................................................................................... 41 2.5.3 Nemerle...................................................................................................... 42 2.5.4 F#e ILX ........................................................................................................ 43 2.5.5 Haskell .NET ................................................................................................ 44 2.6 CONSIDERAÇÕES FINAIS ................................................................................................ 45 3. PHOENIX FRAMEWORK.................................................................................................. 47. 3.1 REPRESENTAÇÃO INTERMEDIÁRIA (IR).............................................................................. 48 3.1.1 Instruções ................................................................................................... 49.

(9) 3.1.2 Operandos................................................................................................. 51 3.1.3 Tipos ............................................................................................................ 52 3.1.4 Unidades .................................................................................................... 54 3.1.5 Símbolos ..................................................................................................... 55 3.1.5.1 Proxy ...................................................................................................................................57. 3.2 FASES E PLUGINS ........................................................................................................... 57 3.3 GERANDO CÓDIGO ..................................................................................................... 60 3.3.1 Gerando código MSIL .............................................................................. 61 3.4 ANÁLISE E OTIMIZAÇÃO ................................................................................................. 62 3.5 CONSIDERAÇÕES FINAIS ................................................................................................ 62 4. PROJETO E IMPLEMENTAÇÃO ....................................................................................... 64. 4.1 OBJETIVOS .................................................................................................................... 64 4.2 ARQUITETURA ................................................................................................................ 64 4.2.1 STG .............................................................................................................. 67 4.2.2 Core to STG................................................................................................ 68 4.3 PHXSTGCOMPILER ....................................................................................................... 72 4.3.1 Lista de fases ............................................................................................. 75 4.3.2 Estratégia de compilação....................................................................... 77 4.3.3 Ambiente de execução .......................................................................... 79 4.4 CONSIDERAÇÕES FINAIS ................................................................................................ 82 5. ANÁLISE E OTIMIZAÇÃO ............................................................................................... 84. 5.1 METODOLOGIA ............................................................................................................. 84 5.2 CÓDIGO .NET GERADO COM O USO DO PHOENIX ........................................................ 86 5.2.1 Variáveis temporárias............................................................................... 87 5.2.2 Casamento de padrões aninhados....................................................... 89 5.3 ANÁLISES E OTIMIZAÇÕES .............................................................................................. 91 5.3.1 Tail call........................................................................................................ 91 5.3.2 Desvios em chamadas recursivas .......................................................... 94 5.3.3 Casamento de padrões com valores booleanos................................ 96 5.4 ANÁLISE FINAL DO COMPILADOR ................................................................................... 97 5.4.1 Versus Haskell .NET .................................................................................... 98 5.4.2 Versus GHC nativo .................................................................................. 100 5.5 CONSIDERAÇÕES FINAIS .............................................................................................. 101 6. CONCLUSÕES E TRABALHOS FUTUROS ....................................................................... 104. 6.1 RESUMO DAS CONTRIBUIÇÕES ...................................................................................... 104 6.2 LIMITAÇÕES E TRABALHOS FUTUROS............................................................................... 105.

(10) APÊNDICE A. -UNIDADES. DE. COMPILAÇÃO. 114 APÊNDICE B. - PROFILER DE MEMÓRIA............................................................................. 119. APÊNDICE C. -PLUGIN 121. DE. RECURSÃO. ATRAVÉS. DE. DESVIOS.

(11)

(12) LISTA DE FIGURAS  Figura 1. Ambiente .NET ............................................................................................................ 1 Figura 2. Visão geral da plataforma Phoenix. Adaptada da documentação do Phoenix[45]................................................................................................................................ 48 Figura 3. HIR da instrução x = add x, *p. Adaptada da documentação do Phoenix[45]................................................................................................................................ 50 Figura 4. Hierarquia de unidades. Adaptada da documentação do Phoenix[45].... 55 Figura 5. Funcionamento de um plugin Phoenix. Adaptada da documentação do Phoenix[45]................................................................................................................................ 58 Figura 6. Dump HelloWorld..................................................................................................... 60 Figura 7. Inserção do PhxSTGCompiler .................................................................................. 1 Figura 8. Processo de compilação ......................................................................................... 1 Figura 9. Arquitetura do compilador.................................................................................... 74 Figura 10. Árvore de compilação......................................................................................... 75 Figura 11. Lista de fases .......................................................................................................... 76 Figura 12. Ambiente de execução ...................................................................................... 81.

(13) LISTA DE TABELAS . Tabela 1 - Comparação entre implementações ................................................................ 1 Tabela 2. Configuração do Ambiente ................................................................................ 85 Tabela 3. Impacto da remoção de variáveis temporárias.............................................. 88 Tabela 4. Remoção de desvios e variáveis desnecessárias ............................................ 91 Tabela 5. Impacto da inserção de instrução tail............................................................... 93 Tabela 6. Informações sobre o coletor de lixo após a inserção de instruções tail ..... 94 Tabela 7. Recursão através de desvio para o inicio da função..................................... 95 Tabela 8. Impacto da remoção de construtores em desvios condicionais................. 97 Tabela 9. PhxSTGCompiler x Haskell .NET............................................................................. 98 Tabela 10. Compilação com informações ausentes na CORE ...................................... 99 Tabela 11 - PhxSTGCompiler* x Haskell .NET. *Com alterações manuais ...................... 99 Tabela 12. PhxSTGCompiler x GHC..................................................................................... 100 Tabela 13. Perfil do consumo de memória (PhxSTGCompiler)...................................... 101 Tabela 14. Unidades básicas ............................................................................................... 114 Tabela 15. Unidades de compilação que representam expressões ........................... 115 Tabela 16. Unidades de compilação atômicas .............................................................. 117 Tabela 17. Unidades de compilação que representam alternativas.......................... 118.

(14) LISTA DE CÓDIGOS  Código 1. Funções curry e não-curry................................................................................... 23 Código 2. Função length não polimórfica ......................................................................... 25 Código 3. Função length polimórfica.................................................................................. 25 Código 4. Tipo algébrico ListInt ............................................................................................. 25 Código 5. Tipo algébrico genérico ...................................................................................... 26 Código 6. Exemplo de closure .............................................................................................. 32 Código 7. Representação de closures utilizando uma classe abstrata........................ 34 Código 8. Representação de uma função utilizando closure e delegates................. 36 Código 9. Exemplo de aplicação de uma função desconhecida............................... 36 Código 10. ListInt C# ............................................................................................................... 38 Código 11. Casamento de padrão utilizando switch....................................................... 39 Código 12. Criação do tipo função .................................................................................... 53 Código 13. Criando uma classe MSIL .................................................................................. 54 Código 14. Criação de tabela de símbolos e adição de um mapeamento por nome ..................................................................................................................................................... 56 Código 15. Construindo uma fase........................................................................................ 59 Código 16. Construindo um Plugin ....................................................................................... 59 Código 17. Transformação HIR para LIR em máquina .NET ............................................. 61 Código 18. Transformando uma expressão em um argumento atômico utilizando let ..................................................................................................................................................... 69 Código 19. Transformando uma expressão em um argumento atômico utilizando case............................................................................................................................................ 69 Código 20. Exemplo unidades de compilação................................................................. 75 Código 21. Variáveis temporárias ........................................................................................ 87 Código 22. MSIL sem remoção de variáveis temporárias................................................ 88 Código 23. Instruções desnecessárias em casamento de padrões aninhados.......... 90 Código 24. Código após a remoção dos desvios e variáveis desnecessárias............ 90 Código 25. Função recursiva para teste de tail-calls ....................................................... 92 Código 26. Chamadas mutuamente recursivas................................................................ 95 Código 27. Representação de desvios condicionais com construtores para valores booleanos. ................................................................................................................................ 96.

(15) Código 28. Representação de desvios condicionais otimizada .................................... 97 Código 29. Ferramenta de profiler de memória.............................................................. 120 Código 30. Plugin que substitui recursão por desvios incondicionais. ......................... 122.

(16) 16. 1. INTRODUÇÃO . Este capítulo apresenta uma visão geral do trabalho e está organizado da seguinte forma: • A Seção 1.1 apresenta os fatores que motivaram o presente trabalho, dando uma breve introdução sobre linguagens funcionais, máquinas virtuais gerenciadas e motivação para integrá-las. • A Seção 1.2 descreve a estrutura da dissertação, apresentado os assuntos discorridos em cada capítulo.. 1.1 Contexto e Motivação  Linguagens funcionais se caracterizam por tratar funções como unidade fundamental de um programa. Desta forma, um programa é constituído por um conjunto de funções que representam sub-partes do problema a ser resolvido. Este tipo de divisão do problema representa uma forma de modularizar ainda mais um problema, pois funções representam problemas específicos a serem resolvidos que podem ser utilizados em mais de uma solução. Diferentemente de linguagens imperativas, nas quais funções são tratadas como uma série de instruções, em linguagens funcionais elas são tratadas como expressões matemáticas. Na programação funcional é evitado uso de estados ou dados mutáveis e a execução de uma função, quando submetida aos mesmos argumentos, sempre retorna o mesmo valor o que garante a ausência de efeitos colaterais e facilita o processo de prova da correção de um programa [ HYPERLINK \l "Hughes1989" 1 ]. Versões mais recentes de linguagens de grande popularidade, tais como Java e C#, têm incorporado algumas destas características, antes só encontradas em linguagens funcionais, numa clara demonstração da importância e poder de expressão destas. Polimorfismo paramétrico, através de generics, e closures1 são Inserida a partir da versão 2.0 do C# através de anonymous delegates e incrementado na versão 3.0 com a criação de expressões lambdas. Para a linguagem Java closures se encontra em fase de análise da proposta[ HYPERLINK \l "Bra08" 67 ], a ser incorporada na versão 7. 1.

(17) 17. exemplos dos recursos incorporados a estas linguagens. Tendo em mente este interesse de linguagens orientadas a objetos em características típicas do paradigma funcional, surge uma pergunta: porque tais linguagens não têm seu uso difundido fora do mundo acadêmico? Um dos principais fatores que dificulta a expansão destas linguagens é a ausência de uma infra-estrutura de desenvolvimento que forneça ferramentas e APIs capazes de aumentar a produtividade e permita o uso das mais recentes tecnologias. Plataformas como Java (JVM) e .NET, fornecem aos programadores tais ferramentas e APIs permitindo um enorme ganho em produtividade e uma rápida integração com os modelos e tecnologias de desenvolvimento mais recentes. Outra característica importante provida por estas plataformas é o uso de máquinas virtuais e código intermediário. Esta característica fornece uma maior abstração sobre a máquina alvo, permitindo que programas e compiladores sejam desenvolvidos sem se preocupar com o hardware ou sistema operacional onde irão trabalhar. O ambiente .NET destaca-se por prover suporte a múltiplas linguagens de programação, permitindo que programas sejam construídos utilizando qualquer uma das linguagens suportadas, podendo ainda, um programa ser constituído de módulos, escritos em linguagens diferentes, que interagem entre si. Além de já prover inúmeras linguagens (C#, J#, C++, VB .NET, etc.), o ambiente .NET permite fácil incorporação de novas linguagens, desde que, estas sigam as especificações do Common Language Runtime (CLR)2,3]. O CLR é a implementação da Microsoft para a Common Language Infrastructure (CLI)[ HYPERLINK \l "ECMA335" 4 ], a qual define um rico sistema de tipos e uma máquina virtual capaz de executar de forma eficiente códigos provenientes de diversas linguagens. Embora de forma não restritiva, o CLR foi desenvolvida com foco na implementação de linguagens que seguem os paradigmas imperativo e orientado a objetos. Desta forma, mapear características de linguagens funcionais, tais como: função de alta ordem, mecanismo lazy evaluation e polimorfismo paramétrico, na plataforma .NET representam um desafio. Diminuir este gap semântico através de estruturas que mapeiem, eficientemente, características comuns a linguagens funcionais na plataforma .NET é objetivo comum de diversos projetos, tais como: Haskell .NET5], ILX[ HYPERLINK \l "Syme2001" 6 ], Mondrian .NET7], Bigloo .NET[.

(18) 18. HYPERLINK \l "Bres2004a" 8 ] e Nemerle9]. Cada uma destas implementações define suas próprias estruturas de mapeamento, não havendo um consenso sobre qual a melhor forma de se representar tais características no ambiente .NET. De modo geral a implementação das estruturas propostas no ambiente gerenciado fornecido pela CRL não possui um bom desempenho, o que abre caminho para estudos de técnicas mais eficientes. Para mapear tais funcionalidades de forma eficiente é necessária uma série de experimentações e testes, de forma a obter estruturas que as represente com o melhor desempenho possível. O framework Phoenix [2], disponibilizado pela Microsoft, é uma ferramenta que tem como propósito facilitar a construção de compiladores e de ferramentas de teste e análise. Ele utiliza uma representação intermediária fortemente tipada para representar um programa e disponibiliza uma grande quantidade de classes e métodos para manipular esta representação. Dentre os recursos disponibilizados, temos o redirecionamento de código para diferentes arquiteturas e plataformas tais como: x86 e MSIL2 e mecanismo de plugin, o qual permite alterar o comportamento de um programa Phoenix sem ter de alterar diretamente seu código fonte. O presente trabalha faz uso do framework Phoenix para a criação e análise de estruturas que mapeiem, eficientemente, as características específicas de linguagens funcionais no ambiente .NET. Espera-se que os recursos disponibilizados pelo framework auxiliem na construção de um compilador que gere códigos mais expertos, ou seja, que usem menos recursos e sejam mais rápidos que os produzidos atualmente.. O. compilador. gerado. servirá. ainda. como. ferramenta. para. experimentação e desenvolvimento de novas técnicas de compilação de linguagens funcionais no ambiente .NET.. 1.2 Organização da Dissertação  Além da introdução esta dissertação conta com mais cinco capítulos e três apêndices, como segue:. 2. Microsoft Intermediate Language.

(19) 19. •. O Capitulo 2 apresenta uma definição geral do paradigma funcional e do ambiente .NET, mostrando suas principais características. Após a apresentação das principais características são demonstradas possíveis abordagens de como implementar uma linguagem funcional no ambiente .NET. Por fim, é feito um resumo das principais implementações de linguagens funcionais existentes.. •. O Capítulo 3 discorre sobre o Framework Phoenix. Nele são apresentadas as principais características e recursos desta ferramenta, sempre que possível através de exemplos práticos.. •. O Capítulo 4 trata da implementação do protótipo. Nele é descrito a arquitetura do compilador, seu ambiente de execução e as decisões de projeto tomadas para geração do código.. •. O Capítulo 5 faz a análise do compilador e mostra o resultado das otimizações e testes realizados. Os primeiros resultados se referem a melhorias na transformação do código IR para MSIL e ao final são exibidos os resultados de otimizações no controle da pilha de execução e em instruções de desvios.. •. O Capítulo 6 aponta as contribuições deste trabalho, restrições e opções para trabalhos futuros.. •. O Apêndice A apresenta tabelas com as classes que representam as unidades de compilação do compilador PhxSTGCompiler.. •. O Apêndice B apresenta o código da ferramenta construída para gerar o perfil de consumo de memória dos programas analisados.. •. O Apêndice C mostra o código de um plugin, utilizado para substituir tail calls por desvios incondicionais em chamadas recursivas..

(20) 20.  .

(21) 21. 2. PROGRAMAÇÃO FUNCIONAL NA PLATAFORMA .NET . Aliar a alta expressividade e o poder de abstração fornecidos por linguagens funcionais a plataformas de alta produtividade como o .NET não é uma tarefa simples. A plataforma .NET tem um modelo de compilação voltado para os paradigmas imperativo e orientado a objeto, o que dificulta o mapeamento de estruturas características de linguagens funcionais. Neste capítulo é feito uma introdução a linguagens funcionais e suas principais características, sendo, em seguida dada uma breve introdução sobre a plataforma .NET. Após discorrer sobre estes conceitos básicos são apresentadas técnicas que permitem mapear linguagens funcionais na plataforma .NET. Finalizando o capítulo, alguns projetos de mapeamento de linguagens funcionais são apresentados descrevendo algumas de suas decisões de projetos.. 2.1 Introdução a Linguagens Funcionais  Linguagens funcionais se caracterizam por tratar funções como unidade fundamental de um programa. Desta forma, um programa é constituído por um conjunto de funções que representam sub-partes do problema a ser resolvido. Diferentemente de linguagens imperativas, nas quais funções são tratadas como uma série de instruções, em linguagens funcionais elas são tratadas como expressões matemáticas. Na programação funcional é evitado uso de estados ou dados mutáveis e a execução de uma função, quando submetida aos mesmos argumentos, sempre retorna o mesmo valor o que garante a ausência de efeitos colaterais e facilita o processo de provar a correção de um programa [ HYPERLINK \l "Hughes1989" 1 ]. Linguagens funcionais são caracterizadas por alta expressividade e grande poder de abstração, decorrentes de construções de alto nível tais como funções de alta ordem, aplicação parcial de funções, avaliação preguiçosa e polimorfismo paramétrico. Estas construções não só aumentam expressividade da linguagem,.

(22) 22. como também a complexidade de sua compilação, especialmente em ambientes orientados a objetos como o .NET. Tais características são melhores especificadas a seguir.. 2.1.1 Funções de alta ordem   Diferentemente de linguagens imperativas e orientadas a objetos, onde há uma clara distinção entre dados e funções, linguagens funcionais não fazem tal distinção, tratando funções como valores de primeira classe. Sendo assim, como qualquer outro valor, elas podem ser passadas como argumentos, retornadas como resultado de outra função, ou ainda armazenadas em estruturas de dados. Uma função é dita de alta ordem quando recebe outra função como um argumento ou computa outra função como seu resultado. Por exemplo, uma função de alta ordem pode atravessar uma lista aplicando uma função recebida como argumento em cada componente da lista10]. Em linguagens funcionais uma função pode ser criada em tempo de execução e referenciar variáveis visíveis apenas onde ela foi declarada. Tais variáveis são denominadas variáveis livres. Os valores referentes a estas variáveis fazem parte da definição da função e por isto a representação de uma função deve conter não só a expressão que a compõe, como também suas variáveis livres. A forma mais direta para esta representação é através de uma closure[ HYPERLINK \l "Minamide1996" 11 ], objeto alocado dinamicamente que encapsula um código a ser executado e um ambiente que pode ser acessado pelo código. Closure não é uma estrutura padrão em ambientes orientados a objetos como o .NET. Alternativas para sua representação serão apresentadas na Seção 2.4.1.. 2.1.2 Aplicação parcial de funções  Linguagens funcionais permitem descrever funções com mais de um argumento como uma composição de funções de um argumento, de forma que.

(23) 23. um argumento seja consumido por vez. Este processo, denominado currificação3 em homenagem a Haskell Curry, altera a concepção, popularizada pelas linguagens imperativas, de que todos os argumentos de uma função devem ser passados ao mesmo tempo, como se fosse uma única estrutura de dados. Embora sua sintaxe favoreça a currificação de funções, Haskell permite a criação de funções sem seu uso, utilizando para isto o conceito de tupla. O exemplo a seguir descreve a mesma função com e sem currificação. 1 2 3 4 5. multiply :: Int -> Int ->Int multiply x y = x*Y multiplyUC :: (Int,Int) -> Int multiplyUC (x,y) = x*Y Código 1. Funções curry e não-curry. A função multiplyUC só é executada ao receber os dois argumentos requeridos através de uma tupla. Já a função multiply permite sua aplicação mesmo passando a ela menos argumentos do que o requerido, obtendo assim, uma função parcial que armazena o argumento recebido e pode ter sua execução completada quando aplicada ao argumento restante. A técnica de executar uma função currificada utilizando menos argumentos do que o número máximo de parâmetros suportados é denominada aplicação parcial[10].. 2.1.3 Avaliação preguiçosa  Uma função nem sempre requer que todos seus argumentos sejam avaliados. Algumas vezes o uso de um argumento depende da avaliação de outra expressão ou mesmo nunca é utilizado dentro do corpo da função. Sendo assim, a decisão de quando deve ser feita a avaliação dos argumentos pode influenciar não só no projeto de uma linguagem como também no seu desempenho. Segundo David Watt[10], quanto ao momento em que é feita esta avaliação, podemos distinguir dois mecanismos:. Embora tenha recebido este nome em Homenagem a Haskell Curry, esta técnica foi inventada por Moses Schönfinkel. 3.

(24) 24. • Eager Evaluation – todos os argumentos são avaliados apenas uma vez, antes da chamada e o valor obtido é ligado a cada ocorrência do parâmetro formal no corpo da função. • Normal-order evaluation – os argumentos são avaliados após a chamada da função, apenas quando requisitados. Ou seja, cada ocorrência do parâmetro formal na função é substituída pela expressão não avaliada. O primeiro mecanismo ao requerer que todos os argumentos sejam avaliados antes da chamada pode gastar um tempo desnecessário em casos onde algum dos argumentos não é utilizado no corpo da função. Já o segundo é menos eficiente em funções onde um determinado parâmetro formal é utilizado mais de uma vez no corpo da função, necessitando que a mesma expressão seja avaliada mais de uma vez. Linguagens funcionais tais como Haskell[12], Mondrian[13] e Lazy ML[14] utilizam um aprimoramento do normal-order evaluation, denominado avaliação preguiçosa, onde cada argumento é avaliado apenas quando necessário e uma única vez. Tal mecanismo além de evitar avaliações desnecessárias permite a criação de estruturas de dados infinitas tais como lazy list[10], onde cada elemento é avaliado sob demanda. Quando uma função sempre usa um determinado argumento, dizemos que ela é estrita para aquele argumento. Sendo assim, linguagens que implementam avaliação preguiçosa ou normal-ordem evaluation são denominadas não estritas, pois podem possuir argumentos que não sendo utilizados nunca serão avaliados.. 2.1.4 Polimorfismo paramétrico  Grande. parte. das. linguagens. funcionais. dá. suporte. a. polimorfismo. paramétrico, onde uma função ou estrutura de dados pode ser definida para operar sobre diversos tipos. No polimorfismo ad-hoc, implementado por linguagens orientadas a objeto através de mecanismos de herança ou sobrecarga, os tipos suportados são restritos e devem ser previamente especificados. Já no polimorfismo paramétrico é permitido o uso de qualquer tipo, devendo a operação que o utiliza ser executada independente do formato do tipo. Como na prática muitas funções.

(25) 25. são naturalmente polimórficas, o polimorfismo paramétrico eleva a expressividade da linguagem. Um exemplo clássico de uma aplicação de polimorfismo paramétrico é a função length, que calcula o número de elementos de uma lista. O código Haskell a baixo implementa a função length para o cálculo de uma lista de inteiros. 1 2 3. length :: [Int]->Int length [] = 0 length (x:xs) = 1 + (length xs) Código 2. Função length não polimórfica. Embora funcione perfeitamente a função definida desta forma é restrita a listas de inteiros. Como as operações executadas em length são independentes do tipo dentro da lista podemos generalizar a função para qualquer tipo. 1 2 3. length::[t]->Int length [] = 0 length (x:xs) = 1 + (length xs) Código 3. Função length polimórfica. Como veremos na Seção 2.1.5 polimorfismo paramétrico também pode ser utilizado para modelar uniões discriminadas, permitindo a construção de tipos de dados complexos que armazenam tipos polimórficos.. 2.1.5 Tipos algébricos  Tipos de dados algébricos formam a base do sistema de tipos da maioria das linguagens funcionais modernas. Eles permitem a definição de tipos estruturados, uniões e tipos recursivos. Um tipo algébrico é um tipo de união discriminada etiquetada[10], onde novos tipos são definidos utilizando construtores (etiquetas) e os tipos dos argumentos. 1. data ListInt = Cons Int List | Nil Código 4. Tipo algébrico ListInt. No Código 4 é definido o novo tipo algébrico ListInt o qual pode conter dois tipos de dados, definidos pelos construtores Cons e Nil. Nil é um construtor vazio, pois não possui nenhum campo, já Cons carrega informações através de argumentos dos tipos Int e List. Desta forma Cons recebe um valor inteiro e um valor do tipo ListInt, ou seja é um tipo recursivo, pois recebe um valor que ele próprio define..

(26) 26. Da mesma forma mostrada com a função length, podemos generalizar tipos algébricos de forma que eles possam representar tipos de dados polimórficos. A definição de List fornecida no Código 5 cria uma lista que pode armazenar qualquer valor suportado pela linguagem. 1. data List t = Cons t (List t) | Nil Código 5. Tipo algébrico genérico. 2.2 Plataforma .NET  A plataforma .NET[15] é um ambiente de desenvolvimento e execução que permite diferentes linguagens de programação e bibliotecas trabalharem juntas na construção de aplicações. A portabilidade destas aplicações também é facilitada, pois um programa criado para a plataforma .NET deve rodar em qualquer dispositivo ou sistema operacional que possua uma implementação de seu ambiente de execução. Com objetivo de ampliar esta portabilidade em diferentes sistemas a Microsoft submeteu o projeto da máquina virtual, Common Language Infrastructure (CLI)[4], para padronização nos órgãos internacionais ECMA[16] e ISO[17]. Desta forma, desenvolvedores de diferentes sistemas operacionais e dispositivos podem construir sua própria versão da CLI capaz de executar aplicativos .NET independente de autorização ou suporte da Microsoft.. 2.2.1 CLR  O CLR é a implementação da Microsoft para o padrão CLI, que define especificações. para. código. executável. e. ambiente. de. execução. da. plataforma.NET. Este ambiente utiliza um compilador Just-In-Time (JIT) que permite a execução de programas traduzidos para uma linguagem intermediária comum (MSIL4[18]), carregando e compilando para código binário partes do código sobre demanda. Este modelo de compilação sobre demanda permite que otimizações sejam feitas de acordo com a plataforma na qual o código é executado. A linguagem intermediária comum implementada na CLR é denominada Microsoft Intermediate Language (MSIL) e não Common Intermediate Language (CIL), como definido pela CLI. Desta forma sempre que for mencionado MSIL entenda linguagem intermediária comum implementada pela CLR. 4.

(27) 27. O processo de compilação e execução de programas, como observado na Figura 1, pode ser descrito nos seguintes passos: 1. O programa escrito em uma das linguagens suportadas pela plataforma (C#, VB.NET, C++, J#, Haskell, etc.) é compilado para uma linguagem intermediária, a Microsoft Intermediate Language (MSIL). 2. Este código MSIL pode fazer chamadas a métodos e classes escritos em outras linguagens que também tenham sido compilados para MSIL, ou ainda para o conjunto de classes da biblioteca .NET. Desta forma o uso de uma linguagem intermediária facilita a interoperabilidade entre diferentes linguagens. 3. O código MSIL é então submetido ao CLR para que seja feita a execução do programa. 4. O CLR, inicialmente, busca por uma versão pré-compilada do código na cache. Caso não encontre ou detecte que a versão resgatada tenha sido alterada é feita a compilação através do JIT. 5. O JIT compilará então cada classe à medida que um método pertencente a esta for requisitado. Isto vale também para métodos provenientes da biblioteca .NET. 6. O código compilado é então executado dentro do ambiente gerenciado .NET, o qual verifica diretivas de segurança e acesso à memória..

(28) 28. Código C#. Código C++. Código. Compilado. Compilado. Compilado. VB .NET. Platafor Código MSIL. Execução Código MSIL Código MSIL. CACHE. Biblioteca. .NET. JIT. 010101010 01000100 10110001 010101111. CLR. Figura 1. Ambiente .NET. 2.2.2 Outras Implementações da CLI  Ao padronizar a CLI a Microsoft possibilitou o surgimento de novas implementações desta para sistemas operacionais e arquiteturas diferentes, promovendo. a. portabilidade. de. programas. .NET.. Dentre. as. diversas. implementações da CLI existentes duas se destacam: a Shared Source CLI (SSCLI ou projeto Rotor)[19] e o projeto MONO[20]. A SSCLI é uma versão de código livre da CLI e do compilador C# implementada pela própria Microsoft para execução no Windows, FreeBSD e Mac OS X5. Esta implementação tem cunho estritamente acadêmico, fornecendo um ambiente de estudo da plataforma .NET e das tecnologias nela empregadas tais como: gerenciamento de memória, coleta de lixo, compilação sob demanda, etc. 5. Apenas para versão 1.0 da SSCLI, a versão 2.0 não disponibiliza mais versões para FreeBSD e Mac OS X..

(29) 29. Por ser voltada para estudo não há uma preocupação quanto ao desempenho, o que foi confirmado em testes comparando o tempo de execução de programas na SSCLI e na CLR[21]. O projeto MONO, financiado pela Novell[22], provê implementações de código livre da CLI para sistemas operacionais Windows, Linux, Unix, Solaris e Mac OS X. É um projeto consistente, com uma grande comunidade de desenvolvedores que incrementa a portabilidade de programas .NET para além do ambiente Windows.. 2.3 Integração à Plataforma .NET  Antes de definir como será feito o mapeamento das estruturas funcionais na plataforma .NET é necessário escolher uma estratégia através da qual será feita tal integração. Esta estratégia define se será utilizado algum mecanismo responsável pela comunicação entre a linguagem e a plataforma ou se será gerado diretamente código suportado por esta.. 2.3.1 Bridge  Permitir a comunicação entre componentes escritos em diferentes linguagens, de forma que, possam trocar informações e acessar recursos uns dos outros é a função de uma bridge, ou “ponte”.. A bridge é responsável por intermediar as. trocas de mensagens, fornecendo uma sintaxe comum, e pela tradução dos parâmetros e valores de retornos, processo este conhecido como marshalling6. Antes mesmo de se integrar linguagens funcionais a ambientes gerenciados, como .NET e Java, esta estratégia já era utilizada para permitir tal integração para código nativo, como é caso de HDirect[23] e GreenCard[24], que implementam a Foreign Function Interface7 (FFI). Em ambientes gerenciados, Hugs .NET[25] e Lambada[26]. Processo de transformação da representação na memória de um objeto em formato apropriado para armazenamento ou transmissão. O processo contrário no qual os dados são novamente transformados em objetos na memória é denominado unmarshalling. 6. 7. Definição da interface para funções externas para linguagem Haskell98..

(30) 30. são exemplos de integração para a linguagem Haskell, respectivamente para as plataformas .NET e Java. Esta é uma estratégia interessante quando o objetivo é obter a integração sem a necessidade de grandes alterações no compilador ou na plataforma, pois toda a complexidade das operações de conversões de tipos e estruturas fica a cargo da bridge. Entretanto esta integração é superficial, no geral apenas chamada de funções, não disponibilizando o acesso a recursos avançados. Outra limitação desta estratégia é quanto ao desempenho, o processo de conversão de tipos é custoso e este overhead deve ser levado em consideração em um projeto de integração. Na plataforma .NET outro fator deve ser considerado: este tipo de integração requer chamadas a código não gerenciado, pois o código gerado pelo compilador funcional gera código nativo, ou seja, não gerenciado pela plataforma . Embora seja permitido este tipo de chamada ela requer que uma série de operações como confirmação de permissões e importação de bibliotecas, que degradam seu desempenho. Há ainda que se considerar que implementações de linguagens funcionais, geralmente, inclui seu próprio ambiente de execução com coletor de lixo e gerenciamento de memória próprios, sendo assim teríamos um cenário onde dois ambientes de execução estariam rodando ao mesmo tempo e consumindo recursos do sistema.. 2.3.2 Compilação   Gerar código suportado diretamente pela plataforma, através de um processo de compilação, é a forma mais direta de integração. Este processo pode tanto ser feito utilizando como destino uma linguagem de alto nível que possua um compilador para o ambiente, como diretamente, gerando código MSIL. A primeira abordagem é mais fácil, pois delega ao compilador da linguagem escolhida a responsabilidade de gerar corretamente o código para a plataforma, além de se valer de otimizações implementadas por esta. A segunda abordagem embora seja mais complexa e susceptível a erros, permite um maior controle sobre o código gerado e uso de instruções não contempladas pelas linguagens de alto nível. Para.

(31) 31. auxiliar a geração direta de código podemos utilizar ferramentas tais como peverify8, ildasm9, ilasm10 e Phoenix. Esta última será detalhada no Capítulo 3. A integração utilizando compilação possui diversas vantagens em relação ao mecanismo de bridge. O compartilhamento de uma mesma representação facilita a comunicação com programas escritos em outras linguagens, reduzindo o overhead causado pelo processo de marshilling/unmarshalling e pela chamada a código não gerenciado. O uso de um mesmo ambiente de execução diminui o uso de recursos do sistema que antes teria que ser compartilhado por dois ambientes com coletores de lixo e gerenciamento de memória separados. A maioria dos projetos de integração de linguagens funcionais à plataforma .NET utilizam a compilação como abordagem. Mondrian[13] e Making Haskell .NET Compatible [27] fazem uso de uma linguagem de alto nível para gerar código enquanto que Nemerle[9] e Haskell .NET[5] geram diretamente código MSIL.. 2.3.3 Estendendo a CLI .  . Os tipos e a linguagem intermediária descritos pela Common Language Infrastructure (CLI) visam proporcionar um ambiente que suporte a implementação de diversas linguagens capazes de interagir entre si, entretanto seu foco é dado a linguagens imperativas e orientada a objetos. Desta forma, faltam a este ambiente estruturas básicas para a representação de funcionalidades comuns a linguagens funcionais.. Modificar a CLI adicionando extensões necessárias para representar. estruturas funcionais facilitaria a compilação de linguagens funcionais para a plataforma .NET. O projeto ILX [28] utilizou esta abordagem, adicionando a CLI novas características como closures, polimorfismo paramétrico, uniões discriminadas e funções de alta ordem. Alterar a máquina virtual permite a implementação de linguagens funcionais com um ganho expressivo no desempenho, além de deixar um legado para futuras 8 Ferramenta, disponibilizada com o framework .NET, que verifica se o código MSIL esta de acordo com as especificações definidas pela CLI. 9. MSIL disassembler. Gera código MSIL a partir de um arquivo PE (DLL ou EXE).. 10. MSIL assembler. Gera um arquivo PE (DLL ou EXE) a partir de código MSIL..

(32) 32. implementações. Entretanto, perde na portabilidade, pois requer que o novo ambiente seja distribuído junto com a linguagem, ou ainda que estas modificações sejam incorporadas a distribuição padrão, o CLR no caso da plataforma .NET . A CLI segue uma padronização, ECMA-335 [4], e a incorporação de novas características a este é dificultada, pois requer aprovação de um conselho de padronização. O projeto F#[29], desenvolvido pela mesma equipe que criou a ILX, faz uso desta última como linguagem alvo do processo de compilação. ILX, por sua vez, é posteriormente traduzido para MSIL, de forma a preservar a compatibilidade com o ambiente padrão de .NET.. 2.4 Mapeando Estruturas Funcionais em Ambientes OO  Para que seja feito o mapeamento de linguagens funcionais em um ambiente OO, como o .NET, faz-se necessário o desenvolvimento de técnicas e estruturas capazes de diminuir o gap semântico entre estes dois mundos. Nesta Seção tais técnicas estruturas serão apresentadas e discutidas.. 2.4.1 Closures  Closures são estruturas essenciais para a representação de linguagens funcionais. Sendo assim o modelo adotado para a representação desta influenciará todo o restante do projeto. Podemos definir uma closure como uma função que armazena todas as variáveis utilizadas por ela, mas que foram definidas fora dela. Tais variáveis são definidas na teoria do cálculo lambda[30] como variáveis livres. Através do exemplo mostrado no Código 6 podemos observar com mais detalhes tais conceitos. 1 2. f1 :: Int -> t -> (Int -> Int) f1 x y = let f2 k = x + k in f2 Código 6. Exemplo de closure. A função f2 definida dentro da função f1, utilizando o comando let, faz uso da variável x definida fora de seu escopo, ou seja, x é uma variável livre da função f2. Ou seja, f2 é uma closure que representa uma função que recebe um argumento k e faz uso de uma variável livre, a qual deve ser encapsulada dentro de sua representação. A função f1 também pode ser considerada uma closure, só.

(33) 33. que sem variáveis livres, o que faz sentido para uma representação única para todas as funções. Em linguagens funcionais, além de representar funções, closures são comumente utilizadas para representar expressões não avaliadas, conhecidas como thunks. Em linguagens com mecanismo de avaliação preguiçosa (Seção 2.1.3) onde a avaliação das expressões é feita apenas uma vez e somente quando necessária, closures são utilizadas para representar a expressão a ser avaliada, armazenando suas variáveis livres e o valor resultante após a avaliação. Closures são, normalmente, implementadas através de estruturas de dados especiais que contém um ponteiro para o código da função e o ambiente léxico da função (conjunto de variáveis livres)[28,31]. Esta abordagem é inviabilizada, ou ainda desestimulada, em ambientes com gerenciamento de memória, como o .NET, onde o uso de ponteiros embora permitido, gera código não verificável11. Ainda que, projetos como o ILX[6] tenham utilizado código não verificável para a construção de closures esta abordagem sofre de restrições de uso, uma vez que a execução de código não verificável requer permissões específicas e não pode se valer das garantias e funcionalidades fornecidas pela CLI. O próprio projeto ILX abandonou tal abordagem em implementações posteriores. Uma alternativa ao uso de ponteiro em código verificável é o uso de estruturas conhecidas como delegates. Delegate é a versão orientada a objetos de ponteiro para função, que permite a chamada de métodos, tanto de instância como estático, de forma segura e verificável. Na implementação 1.0 da CLR havia problemas de desempenho, o que justificou a utilização de ponteiros na ILX, entretanto testes realizados demonstraram que tais problemas foram solucionados a partir da versão 2.0 fazendo com que chamadas a métodos utilizando delegates tenham desempenho semelhante a chamadas a métodos virtuais ou de interface [21].. Código não verificável, no ambiente .NET, significa que o código não segue as restrições de segurança impostas pela CLI não sendo gerenciado diretamente pelo ambiente. 11.

(34) 34. 2.4.1.1 Projetando uma closure  Uma forma bastante direta de se representar closures em ambientes orientados a objetos é através da definição de uma classe abstrata Closure que possui um método Invoke, responsável pela execução da expressão. Neste modelo para cada closure deve ser criada uma nova classe que herda da classe Closure, armazena suas variáveis livres em campos da classe e sobrescreve o método Invoke de forma que ele execute o código correspondente a avaliação da closure. O Código 7 demonstra como criar uma nova closure estendendo a classe abstrata. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16. //Classe abstrata Closure public abstract class Closure { public abstract object Invoke(); } // Criando uma nova closure class newClosure : Closure { // Campos representando variáveis livres public override object Invoke() { //Código da closure } } Código 7. Representação de closures utilizando uma classe abstrata. Para passagem de argumentos para a função Invoke poderia ser utilizado um array de objetos ou ainda uma pilha. F# [29] possui classes abstratas pré-definidas para até cinco argumentos e um valor de retorno, utilizando generics[32] para definição dos tipos. Funções com mais que cinco argumentos são tratadas utilizando aplicações parciais, mecanismo detalhado na Seção 2.5.4. Nemerle[9] utiliza mecanismo semelhante, entretanto possui classes abstratas pré-definidas para até vinte argumentos, além de permitir chamadas não currificadas utilizando para tanto uma tupla contendo todos os argumentos da função. É importante observar que embora hajam classes pré-definidas para cada nova closure definida deverá ser. produzida. uma. nova. classe. que. herde. da. classe. correspondente,. sobrescrevendo seu método Invoke e adicionando campos para suas variáveis livres. Tanto F# como Nemerle são linguagens estritas, o que reduz o número de closures geradas, uma vez que, não são necessárias novas closures para representar computações não avaliadas. Entretanto, a geração de uma classe por closure em.

(35) 35. linguagens funcionais não estritas, como Haskell, resultaria em uma grande quantidade de classes. Segundo Don Syme [6], estima-se que seja encontrado na biblioteca padrão do GHC uma closure por linha de código Haskell. Como na plataforma .NET a cada classe são associados metadados que necessitam ser carregados e checados durante a execução do programa, uma enorme quantidade de classes causariam uma queda no desempenho do código produzido. Visando diminuir o número de classes geradas e conseqüentemente a queda de desempenho o projeto Haskell .NET [5] utilizou a abordagem da construção de classes pré-definidas para closures com n variáveis livres e adotou um mecanismo de pilha para a passagem dos argumentos. Neste, ao invés de ser gerada uma nova classe para representação de cada closure, todas as closures que possuem a mesma quantidade de variáveis livres serão representadas através de instâncias de uma mesma classe pré-definida no ambiente de execução da linguagem. O que diferencia as diversas instâncias da mesma classe será a função armazenada, correspondente. ao. código. da. closure.. No. projeto. Haskell. .NET. para. o. armazenamento desta função é utilizada um delegate ao invés de um ponteiro ou método abstrato. O Código 8 mostra como criar uma closure para representar a função f2 mostrada no Código 6. Nas linhas 2 e 3 é criado o delegate que armazena a função com o código de f2. Como será mostrado na Seção 2.4.2.1, utilizando o modelo push/enter o delegate não armazena diretamente a função com o código correspondente a expressão, mas sim, uma função auxiliar. As linhas 6 e 7 são responsáveis por construir a closure que representa a função. Pode-se observar que a classe utilizada para representar a closure possui um tipo genérico, este tipo genérico representa o tipo da variável livre armazenada pela closure, que neste caso é instanciado como sendo do tipo inteiro. Na linha 10 é configurado o valor da aridade da função. Este valor, como será visto na Seção 2.4.2 é útil para definir se a aplicação da função é saturada ou não. Por último, na linha 13, o valor da variável livre é adicionado a closure. 1 2 3 4 5. //Delegate para a função NonUpdCloFunction_1_FV<int> funcDelegate = new NonUpdCloFunction_1_FV<int>(function); //Criação da closure que recebe como argumento o delegate.

(36) 36. 6 7 8 9 10 11 12 13. NonUpdateableClosure_1_FV<int> closure = new NonUpdateableClosure_1_FV<int>(funcDelegate); //Configura a aridade da função closure.arity = 1; //Armazena o valor da variável livre closure.fv1 = x; Código 8. Representação de uma função utilizando closure e delegates. 2.4.2 Mecanismo de aplicação de funções  A combinação de polimorfismo paramétrico, funções de alta ordem e aplicação parcial de funções gera um cenário onde em alguns momentos pode ser necessário efetuar a aplicação de uma função desconhecida em tempo de compilação. No Código 9, f representa uma função desconhecida, uma vez que não se sabe em tempo de compilação como se comportará tal função. Não é possível simplesmente aplicar f aos dois argumentos, pois não se pode afirmar quantos argumentos f espera receber e qual o retorno da aplicação. Esta pode ser uma função que recebe apenas um argumento, processa este e gera uma nova função que consumirá o argumento restante, ou mesmo, uma função que receba mais de dois argumentos e desta forma o resultado de zipwith é uma lista de funções. 1 2 3. zipWith :: (a->b->c)-> [a] -> [b] -> [c] zipWith f [] [] = [] zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys Código 9. Exemplo de aplicação de uma função desconhecida. Para tratar a aplicação de funções desconhecidas em linguagens funcionais existem dois modelos: eval/apply e push/enter. A diferença básica entre os dois modelos é quem será o responsável por tratar em tempo de execução a aplicação da função, se a própria função chamada ou o código que faz a chamada. O uso de um destes mecanismos deve ser efetuado apenas para funções desconhecidas em tempo de compilação, caso contrário a função deve ser chamada normalmente, evitando assim um overhead desnecessário..

(37) 37. 2.4.2.1 Modelo push/enter  No modelo push/enter a própria função será a responsável por, em tempo de execução, verificar a aridade12 da função, o número de argumentos recebidos e decidir como deverá ser feita a aplicação da função. Neste modelo para cada função definida na linguagem duas funções devem ser geradas após a compilação. Uma, denominada fast entry point (FEP), contendo o código correspondente da função original e outra, slow entry point (SEP), com o código responsável por verificar a aridade e o número de argumentos, decidindo que atitude tomar. O processo executado pode ser resumido em duas etapas: • Push: os argumentos passados para a função são empilhados (push) em uma pilha diferente da pilha de execução da CLR. • Enter: é feita a chamada a função SEP que avalia a aridade da função e o número de argumentos presente na pilha e baseado nestas informações determina se o próximo passo será a ou b. a. Caso o número de argumentos presentes na pilha sejam suficientes, estes são desempilhados e a função FEP é executada retornando o valor da avaliação. Argumentos excedentes são mantidos na pilha para que possam ser consumidos posteriormente, provavelmente pelo retorno de FEP. b. Caso o número de argumentos presentes na pilha seja inferior à aridade, estes são desempilhados e utilizados para criar uma aplicação parcial que é retornada como valor da avaliação. Haskell .NET utiliza esta abordagem criando pilhas diferentes para armazenar diferentes tipos de argumentos boxing e unboxing.. 2.4.2.2 Modelo eval/apply  Neste modelo a responsabilidade sobre como tratar a chamada de uma função desconhecida fica a cargo do código que invoca a função (caller). Este código deve, primeiramente, avaliar (eval) a aridade e o número de argumentos e. Aridade pode ser entendido como o número de argumentos que uma função espera receber para realizar sua funcionalidade. 12.

(38) 38. então decidir qual a aplicação (apply) deve ser feita: chamar diretamente a função, caso o número de argumentos seja maior ou igual à aridade, ou criação de uma aplicação parcial a ser retornada, caso contrário. Historicamente a grande maioria dos compiladores para linguagens funcionais lazy utilizam a abordagem push/enter, entretanto após estudos feitos por Marlow e Peyton Jones [33], que demonstraram uma ligeira vantagem do uso do modelo eval/apply em uma implementação do Glasgow Haskell Compiler (GHC), o modelo eval/apply tem ganhado espaço. Na plataforma .NET, ainda não existem estudos que apontem qual modelo apresenta melhor desempenho. Nesta plataforma, o uso do eval/apply teria como vantagem o uso direto da pilha da CLR como mecanismo de passagem de parâmetros, o que não é possível no modelo push/enter devido a restrições na manipulação direta da pilha impostas pela CLR. Entretanto, o modelo eval/apply pode gerar aplicações parciais desnecessárias, não geradas utilizando o push/enter [33]. F# e Nemerle são exemplos de utilização de eval/apply na plataforma .NET.. 2.4.3 Representação de tipos algébricos  Na plataforma .NET não existe o conceito de tipos algébricos como em linguagem funcionais. O mais perto que há são as enumerações que permitem que se descreva um tipo através de um conjunto de constantes, entretanto enumerações não permitem o uso de argumentos. O uso de uma classe abstrata para representar um tipo algébrico e subclasses destas para representar as possíveis construções é uma das abordagens mais utilizadas em ambientes orientados a objetos [34,35,36]. Utilizando tal abordagem ListInt (Código 4) teria a seguinte representação em código C#. 1 2 3 4 5 6 7. public abstract class ListInt{} public class Nil : ListInt {} public class Cons : ListInt { public int val; public ListInt list; } Código 10. ListInt C#.

Referências

Documentos relacionados