Archive for Agosto 17th, 2008
Um pouco de Erlang, programação funcional e recursividade
Comecei a estudar um pouco de Erlang recentemente. A linguagem realmente é bem diferente daquilo que estou acostumado, principalmente por ser um linguagem totalmente funcional e possuir certos conceitos que em um primeiro momento realmente podem confundir a cabeça de quem está mais acostumado com linguagens imperativas.
Na verdade, eu já acabei usando diversos conceitos de programação funcional em Ruby e até mesmo em Javascript. A existência de closures ajuda muito para que isso seja possível e para muitos problemas uma solução funcional pode se apresentar a mais natural.
Entretanto, em Erlang a coisa é diferente. Tudo é baseado em programação funcional.
Uma das estrututas de dados mais comumente utilizadas em praticamente qualquer linguagem de programação são as listas. Quer essas listas sejam representadas por simples arrays ou por estruturas mais complexas como listas encadeadas, as listas estão presentes sempre que desejamos realizar qualquer tipo de operação sobre conjuntos.
Em Erlang, as listas são representadas através de estruturas do tipo [el1, el2, el3, ..., eln], onde eli pode assumir formas mais complexas, como em Lista = [{banana, 3}, {pera, 5}]. Neste exemplo estamos representando, por exemplo, uma lista de compras que contém 3 bananas e 5 pêras. O problema é não confundir as palavras “banana” e “pera” com strings ou variáveis: neste exemplo, banana e pera são atoms, ou seja, apenas identificadores para elementos da lista.
Outro detalhe importante é que em Erlang um símbolo somente será considerado como uma variável se começar por uma letra maiúscula.
Mas vamos ver então em detalhes como Erlang opera sobre listas!
Vamos partir de um exemplo bastante simples. Suponha que você possui uma lista de números inteiros e deseja calcular o produtório desta lista, ou seja, sair multiplicando número por número até chegar em um total.
Em uma linguagem imperativa como C, isso poderia facilmente ser feito assim:
#include <stdio.h>
int main() {
int lista[5] = {3, 5, 7, 2, 8};
int prod = 1, i = 0;
for(i = 0; i < 5; ++i) { prod *= lista[i]; }
printf("Produto = %d\n", prod);
}
Até aqui, ok, Basta fazermos um laço e sair multiplicando os números dentro do array prod que representa nossa lista.
Mas e em Erlang, como ficaria isso? Bom, primeiro precisamos entender como Erlang processa os elementos de uma lista. Em Erlang, pode-se assumir que toda e qualquer lista é formada de duas partes: HEAD e TAIL. HEAD é sempre o primeiro elemento da lista, ou seja, um escalar. Já TAIL representa o restante da lista. Assim, na lista [1,2,3,4] temos HEAD = 1 e TAIL = [2,3,4]. Na verdade os nomes HEAD e TAIL são apenas convenções, não é obrigatório que você use estes termos para representar as duas “partes” das listas em Erlang. Mas convenções são boas e facilitam a compreensão do código que escrevemos em qualquer linguagem.
Como podemos então extrair e armazenar em variáveis os valores de HEAD e TAIL de uma lista? Usando pattern matchers. Na verdade, em Erlang todas as atribuições de valores a variáveis são feitas através da utilização de pattern matchers. O operador utilizado para isso é o =. Mas aqui, = tem um significado diferente do que você está acostumado! Sim! Igual não é mais igual e o mundo está perdido…
A verdade é que em Erlang = não representa necessariamente igualdade. = serve para “casar” padrões. Cada variável pode armazenar um determinado padrão e você pode utilizar o operador = para verificar se este padrão “casa” com algum outro e então fazer associações de valores dentro destes padrões. Assim:
%% Em Erlang, linhas que começam com % são comentários. %% Todas as linhas que representam "comandos" devem terminar com um ponto 1> X = 2.
Ok, agora a variável X vale 2. Mas isso porque antes disso não associamos nenhum outro valor a X, ela era uma variável “vazia”, ou, usando a terminologia do Erlang, X era uma unbound variable. Agora, se tentarmos alterar o valor de X, olhe o que acontece:
2> X = 4.
=ERROR REPORT==== 17-Aug-2008::14:34:44 ===
Error in process with exit value: {{badmatch,4},[{erl_eval,expr,3}]}
** exited: {{badmatch,4},[{erl_eval,expr,3}]} **
Que erro bizarro! Que porcaria de linguagem é essa que não me permite alterar o valor de uma variável? O que ocorre é que Erlang é uma linguagem para processamento paralelo, onde diferentes processos trocam mensagens entre si. Se temos por exemplo 3 processos A, B e C que se comunicam entre si e uma variável X que é usada pelos 3 processos, se A passar o valor para B e este alterá-lo, quando C for usar esta variável pode ser que ocorram resultados inesperados. Por isso Erlang mantém as variáveis imutáveis durante toda sua existência.
Partindo deste princípio, podemos fazer o seguinte:
1> X = 2. %%X era 'unbound' e acabou de receber seu valor (2) 2> X = 1 + 1. %% funciona, porque 1 + 1 representa o mesmo padrão que 2
Ok, vamos parar de enrolar e voltar para a nossa lista, de onde queremos extrair nossas partes HEAD e TAIL.
Podemos usar o operador | (nosso velho amigo ‘pipe’) para fazer o trabalho. Podemos então escrever:
Numeros = [3, 5, 7, 2, 8]. [H|T] = Numeros.
E após isso teremos a variável H armazenando 3 (nosso HEAD) e a variável T armazenando [5, 7 , 2, 8] (o resto da lista ou TAIL).
Para podermos calcular nosso produtório precisamos ainda dar um olhadinha em como funcionam funções em Erlang. O processo todo funciona através da utilização de modules. Um module é um elemento de software onde você pode declarar suas funções. Em seguinda você precisa exportar essas funções para que as mesmas possam ser utilizadas por código fora do módulo. Algo assim:
-module(produtorio). -export([prod/1]). prod([H|T]) -> H * prod(T); prod([]) -> 1.
Na primeira linha estamos fazendo a declaracao de um modulo chamado “produtorio”. Na segunda linha estamos exportando uma função chamada “prod” que recebe um único argumento (por isso o “/1″), para que a função possa ser usada por código em outras partes do programa.
A terceira e quarta linhas definem nossa função prod, a qual efetivamente realiza o nosso cálculo. Para explicar o que está acontecendo aqui, precisamos relembrar alguns conceitos de recursividade.
Uma função recursiva é uma função que, para atingir seu objetivo, realiza chamadas a si mesma. Assim, podemos por exemplo escrever uma função recursiva soma([1,2,3]) como soma(1, soma([2,3])).
Por definição, uma função recursiva deve possuir duas características:
1) Deve possuir uma solução trivial, ou seja, uma situação onde a função pára de chamar a si mesma recursivamente e a pilha de chamadas passa a ser desfeita, até a primeira chamada feita, onde o resultado finalmente é retornado ao ponto do código que fez a chamada a nossa função.
2) A função deve ser escrita de forma que caminhe naturalmente para a solução do problema ou, em outras palavras, a função deve ser convergente, caso contrário as chamadas recursivas ocorrerão infinitamente, levando a um estouro na pilha de chamadas da função.
Essas duas características podem ser facilmente observadas na nossa função prod acima. Na linha
prod([H|T]) -> H * prod(T);
temos a forma em que as chamadas recursivas ocorrerão, o que demonstra claramente que há convergência para a solução. A função recebe uma lista de valores, extraí o primeiro elemento da lista e retorna como resultado desta chamada específica o resultado da multiplicação deste elemento pelo resultado de uma chamada a si mesma, passando o restante da lista (ou TAIL) como argumento. É fácil observar que a cada nova chamada, TAIL diminui, até chegar a 0 (zero) elementos. Essa é a nossa solução trivial, encontrada pela linha
prod([]) -> 1.
Aqui, não é mais necessário realizar chamadas recursivas, pois sabe-se que para a lista vazia basta multiplicar por 1, ou seja, o elemento neutro da multiplição. A partir deste ponto a pilha de chamadas recursivas passa a ser esvaziada até chegarmos à sua basse, ou seja, a primeira chamada feita à função prod().
Por fim, podemos usar nossa função assim (através do erl, o console do Erlang):
1> c(produtorio).
{ok,produtorio}
2> Lista = [3,6,8,4,7].
[3,6,8,4,7]
3> P = produtorio:prod(Lista).
4032
4>
Em (1) estamos compilando nosso module produtorio para um arquivo com extensão .bean, para que possamos usá-lo por todo nosso programa. Em (2) declaramos uma variável Lista, que representa uma lista de valores sobre os quais desejamos calcular o produtório. Em (3) realizamos efetivamente a chamada à função prod. A sintaxe produtorio:prod indica que a função prod faz parte do module produtorio. Você pode pensar nisso como um esquema de namespaces.
O cálculo ocorre seguindo a seguinte lógica:
Chamada 1) HEAD -> 3, TAIL -> [6,8,4,7], retorno -> 3 * prod([6, 8, 4, 7])
Chamada 2) HEAD -> 6, TAIL -> [8,4,7], retorno -> 18 * prod([8,4,7])
Chamada 3) HEAD -> 8, TAIL -> [4, 7], retorno -> 144 * prod([4, 7])
Chamada 4) HEAD -> 4, TAIL -> [7], retorno -> 576 * prod([7])
Chamada 5) HEAD -> 7, TAIL -> [], retorno -> 4032 * prod([])
Chamada 6) Utiliza-se a solução trivial, retornando-se 4032 * 1 e dando fim à cadeia de chamadas recursivas.
Conforme eu for estudando mais coisas sobre Erlang eu vou postando aqui o que achar interessante!


