PARADIGMAS DE PROGRAMAÇÃO 1º semestre de 2005 Profª Evelyn Cristina Assunto: Linguagem Funcional Características das Linguagens Imperativas As linguagens imperativas (Pascal, C, entre outras) são caracterizadas por três conceitos: variáveis, atribuições e laços de iteração. O estado de um programa é mantido nas variáveis do programa. Essas variáveis são associadas a localizações de memória caracterizadas por um endereço e por um valor armazenado no endereço. Nós podemos acessar o valor diretamente, lendo o valor da variável, ou indiretamente, através de ponteiros que levam ao valor da variável. O valor da variável é modificado através de atribuições. Por exemplo, em Pascal: X := 5; O termo à esquerda do operador de atribuição (:=) é a variável cujo valor está sendo modificado e à direita está o novo valor. X tem valores diferentes antes e depois da atribuição. Portanto o significado (efeito) de um programa depende da ordem em que as atribuições são escritas e executadas. Já na Matemática, as variáveis são associadas a valores e, uma vez que o fazem, elas não mudam mais seus valores. Portanto, o valor de uma função não dependem de tais conceitos de ordem de execução. Ao invés disso, uma função matemática define uma mapeamento de um valor do domínio para um valor do conjunto imagem. É um conjunto de pares ordenados que relacionam unicamente cada elemento do domínio ao seu elemento correspondente no conjunto imagem. As funções em linguagens imperativas, por outro lado, são descritas como algoritmos que especificam como computar uma faixa de valores de um domínio com uma série pré-escrita de passos. Uma última característica importante das linguagens imperativas é que a repetição (laços) é utilizada extensivamente para computar valores desejados. Laços são usados para varrer um vetor ou acumular valores numa variável. Em contraste, nas funções matemáticas os valores são computados através de aplicações de funções. Recursão é utilizada no lugar de iteração. Composição de funções é também utilizada para compor funções mais poderosas. Por causa dessas características, as linguagens imperativas têm sido chamadas de orientadas a estado ou orientadas à atribuição. Em contraste, as linguagens funcionais têm sido chamadas de baseadas em valores e aplicativas. Funções Matemáticas Uma função é uma regra de mapeamento (ou associação) de membros de um conjunto (o conjunto domínio) a membros de um conjunto imagem. Por exemplo, a função dobro mapeia um conjunto de números a naturais a um subconjunto de números naturais. O dobro de 0 é 0 O dobro de 1 é 2 O dobro de 2 é 4 O dobro de 3 é 6 O dobro de 4 é 8 f(x) = 2*x Domínio={0, 1,2, 3, 4, ...} e Imagem={0, 2, 4, 6, 8, ...} Uma vez que a função foi definida, ela pode ser aplicada a um elemento particular do conjunto domínio: a aplicação leva à associação de um elemento no conjunto imagem. Se uma função F é definida em composição de duas outras G e H, ela é escrita assim: F G o F, Onde a aplicação de F é definida como a aplicação de H e então aplicar G ao resultado de H. Muitas funções matemáticas são definidas recursivamente, isto é, a definição da função contém uma aplicação da própria função. Por exemplo: n!1,=se {n=0 n*(n-1)!, se n>0 Domínio= N e Imagem=N Ou seja, o fatorial de um número é definido na multiplicação desse número pelo fatorial do número natural anterior. Programação Recursiva É possível fazer programas recursivos também em linguagens imperativas. Por exemplo, abaixo está uma implementação da função fatorial em C. int fatorial(int n){ if (n == 0) return 1; else return ( n * fatorial( n – 1 ) ); } Se num programa chamamos essa função da seguinte forma: X := fatorial(4); Então, ela será executada da seguinte forma: N=4 -> 4 * fatorial(3) N=3 -> 3 * fatorial(2) N=2 -> 2* fatorial(1) N=1 -> 1 * fatorial(0) N=0 -> 1 Como temos o fatorial de 0, assim podemos calcular o fatorial de 1 (1 * 1 = 1). Uma vez já conhecido o fatorial de 1, obtemos facilmente o fatorial de 2 (2 * 1 = 2). Agora é possível computarmos o fatorial de 3( 3 * 2=6). E finalmente chegamos ao fatorial de 4 (4 * 6=24). A função fatorial(4) retorna 24. Outro exemplo: int fibonacci (int n) { int s1, s2 ; if (n == 0) return 1; else if (n == 1) return 1; else { s1 = fibonacci(n-1); s2 = fibonacci(n-2); return s1 + s2; } } Exercício: simule a execução recursiva da função fibonacci(6). Princípios de Linguagens Funcionais Uma linguagem de programação funcional tem três componentes principais: 1. Um conjunto de dados. Tradicionalmente, as linguagens de programação funcionais oferecem mecanismos de alto nível para tratamento de estruturas como listas. 2. Um conjunto de funções pré-construídas para manipular objetos básicos de dados. Por exemplo, LISP e ML oferecem várias funções para tratamento e construção de listas. 3. Um conjunto de formas funcionais para construção de novas funções. Um exemplo comum é a composição de funções. Lambda Calculus Lambda calculus é um cálculo que modela os aspectos computacionais das funções. Estudar lambda calculus ajuda a entender os elementos da programação funcional e semântica das linguagens de programação funcionais independentemente dos detalhes de sintática de uma linguagem particular. Lambda calculus leva esse nome devido à letra grega (lambda), que é usada na notação. As expressões do lambda calculus são de três tipos: e1 – um expressão pode ser um identificador único como x, ou uma constante como 3. e2 – uma expressão pode ser uma definição de função. Tal expressão tem a forma (x.e). A expressão e representa o corpo da função e x o parâmetro da função. A função quadrado por exemplo, é definida assim: x.x*x. e3 – uma expressão pode ser uma aplicação da função. Uma aplicação de função tem a forma (e1 e2), onde a função e1 é aplicada à expressão e2. Por exemplo, a função quadrado pode ser aplicada ao valor 2 dessa maneira: ((x.x*x) 2). LISP O LISP original, introduzido por John McCarthy em 1960 e conhecido como LISP puro, é uma linguagem completamente funcional. Ela introduziu muitos novos conceitos de linguagens, incluindo o tratamento uniforme de programas como dados, expressões condicionais, coleta de lixo automática e execução interativa de programas. Objetos de dados LISP é uma linguagem para computação simbólica. Valores são representados por expressões simbólicas. Uma expressão pode ser um átomo ou uma lista. Um átomo é uma cadeia de caracteres (letras, dígitos e outros). Os seguintes são átomos: A AUSTRIA 68000 Uma lista é uma seqüência de átomos oulistas, separados por espaço e parênteses. Os seguintes são listas: (PLUS A B) ((CARNE FRANGO) (BROCOLIS BATATA TOMATE) AGUA) Uma lista vazia é também chamada de NIL. A lista é o único mecanismo para estruturar e codificar informação em LISP puro. Um símbolo (ou átomo) é número ou um nome. Um número representa um valor diretamente. Um nome representa um valor associado ao nome. Há modos diferentes de associar um valor a um nome: SET associa um valor globalmente e LET associa localmente. (SET X (A B C)) associa X a uma lista (A B C). Funções Existem muito poucas funções primitivas oferecidas pelo LISP puro. A função aspa simples (‘) dá o literal. Por exemplo, ‘A representa a letra A e não uma variável A. (CAR ‘(A B C)) retorna A, que é o cabeça da lista de literais (A B C). (CDR ‘(A B C)) retorna a cauda da lista (A B C), que é (B C). (CONS ‘A ‘(B C)) retorna a lista (A B C) (CONS ‘(A B C) ‘(X Y Z)) retorna ((A B C) X Y Z) A função ATOM returna T se o argumento é um átomo e () caso contrário. NULL retorna T se seu argumento é NIL. EQ compare seus argumentos (que deve ser átomos), se eles são iguais. Por exemplo: (ATOM ‘A) = T (ATOM ‘(A)) = NIL (EQ ‘A ‘A) = T (EQ ‘A ‘B) = NIL A função COND serve como uma expressão “case”. Ela recebe como argumento uma lista de pares . Ela é avaliada examinando os pares na seqüência, para encontrar o primeiro par cujo predicado é avaliado como true. Exemplo: (COND ((ATOM ‘(X)) ‘B) (T ‘C)) = C A primeira condição é falsa porque X não é um átomo. A segunda condição é true (T) e funciona como se fosse uma cláusula else. A função x+y.x+y em LISP como (LAMBDA (X Y) (PLUS X Y)) A aplicação de função também segue o modelo de expressão lambda. Por exemplo: ((LAMBDA (X Y) (PLUS X Y)) 2 3) associa X a 2 e Y a 3 e aplica PLUS (soma), resultando 5. A associação de um nome a função é feita através da função DEFINE: (DEFINE (ADICAO (LAMBDA (X Y) (PLUS X Y)))) Agora o átomo ADICAO pode ser usado no lugar da função. Se eu quiser aplicar a função, agora posso fazer assim: (ADICAO 2 3) resulta em 5. Dar um nome à função é especialmente útil para definir funções recursivas. Por exemplo, nós podemos definiar a função INVERTE para inverter os elementos de uma lista: (DEFINE (INVERTE (LAMBDA (L) (INV NILL L)))) (DEFINE INV (LAMBDA (SAIDA ENTRADA) (COND (NULL ENTRADA) SAIDA) (T (INV (CONS (CAR ENTRADA) SAIDA) CDR ENTRADA))))))) A função INVERTE chama a função INV, que trabalha pegando o primeiro elemento da lista e chamando INV no resto da lista. Este programa demonstra duas técnicas utilizadas em programação funcional. LISP aceita a chamada da função INV na função INVERTE antes mesmo de a definirmos. INV tem dois argumentos para computação e são chamados recursivamente.Inicialmente, o segundo parâmetro contém o valor de entrada (a lista a ser invertida) e o primeiro parâmetro contém um lista vazia. Cada chamada a INV remove o primeiro elemento do segundo parâmetro e o insere no início da lista do primeiro parâmetro.Quando o segundo parâmetro é exaurido, ou seja, atinge lista vazia, então o primeiro parâmetro contém a lista invertida. O programa termina, retornando a lista invertida. Bibliografia Ghezzi, Carlo; Jazayeri, Mehdi. Programming Language Concepts. 3ª ed. 1998. Ed. John Wiley & Sons.