sexta-feira, 6 de novembro de 2009

Uma comparação ingênua entre algumas linguagens

Pretendo fazer uma pequena comparação entre algumas linguagens de programação, só para tentar mostrar um pouquinho de como cada uma tende a fazer com que o programador pense nas soluções de uma determinada maneira.

Para tal, vamos começar com uma pergunta bem simples: quem negaria que o conceito de somatório é importante em qualquer ciência exata? Se você não se lembra do somatório, dê uma olhada no artigo sobre somatório da Wikipédia.

Ou seja, essa é uma notação para a soma de f(1) + f(2) + ... + f(m). Um conceito bem simples que torna um monte de coisas bem mais práticas. Se tiver algum matemático lendo esse texto, certamente vai lembrar como o somatório é importante na hora de formalizar o conceito de série de números reais, de série de funções. Até mesmo a integral pode ser definida como um limite de somatórios. Falo da soma de Riemann.

Calma, esse texto não vai ser sobre matemática, não se desanimem. Vou começar com um objetivo bem simples: como levar naturalmente o conceito de somatório para a programação? Em linguagens como em C normalmente o somatório fica escondido dentro dos for's e dos while's, ou dá para fazer um hack usando ponteiros para funções, já em outras linguagens, como Python, Ruby e Scheme o somatório pode ser implementado de uma maneira bem elegante.

Mas antes de começar a implementar, vamos tentar formalizar um pouco o conceito de somatório. Podemos pensar no somatório como uma função que recebe dois números inteiros e uma função e então devolve o resultado da soma da função calculada em cada um dos números inteiros entre os dois inteiros dados.

Como isso ficaria em pseudocódigo? Para quem programou muito em C, eu acho que o código ficaria assim:

real soma(inteiro ini, inteiro fim, funcao f)
{
  inteiro i = ini;
  real r = 0;

  enquanto(i <= fim)
  {
    r = r + f(i)
    i = i + 1;
  }

  devolve r;
}

Mas, será que é fácil implementar algo assim em C? Não é complicado, mas tem uns probleminhas e limitações que ficarão bem óbvios, vejam:

float soma(int ini, int fim, float (*f)(float x))
{
  int i = ini;
  float r = 0;

  while(i <= fim) /* while é mais compreensível do que o for! */
  {
    r = r + f(i);
    i = i + 1;
  }

  return r;
}

Mas, tio, qualé a desse float (*f)(float x)?! - É um ponteiro para uma função! Sim, existe esse treco e dá tanta dor de cabeça quanto você pode imaginar. :D

O problema aqui é que em C funções são diferentes dos dados comuns. Outro problema chato é que a soma funciona para um tipo específico de função e tentar aumentar a flexibilidade só causaria mais dor de cabeça, mais náuseas - deixa eu explicar com termos "técnicos" - você teria que usar mais ponteiros. Sim, nada é tão complicado e estranho que não possa ser piorado.

Agora vamos dar uma olhada como essas coisas ficam em Python:

def soma(ini, fim, func):
  r = 0
  for i in range(ini, fim + 1):
    r = r + func(i)
  return r

Um amigo sugeriu que a maneira mais pythônica de fazer isso é:

def soma(ini, fim, func):
  return sum(func(i) for i in range(ini, fim+1))

A primeira coisa que notamos é como a sintaxe é simples e elegante e o código, menor! Mas acho que todo mundo já está cansado de saber como Python é uma linguagem linda e elegante. Vamos para a parte que importa.

Além disso a função func é tratada como um dado normal, que pode ser passada como parâmetro e depois pode ser chamada como função. Além disso, outra vantagem óbvia do Python é que não precisamos nos preocupar demais com o tipo de dado que a função fim devolve. Basta que ela devolva tipos de dados somáveis. Em C teríamos que fazer uma função de soma para funções que devolvem inteiros, depois outra que soma (concatena) strings ou algum outro tipo de dado em que seja possível definir uma soma.

Vamos ver como o mesmo código fica em Scheme?

(define (soma ini fim f)
  (if (> ini fim)
      0
      (+ (f ini) (soma (+ ini 1) fim f))))

Não é simples e bonito? O que eu achei mais interessante é que pela maneira com que o scheme foi desenvolvido, não há a necessidade coisas como o while e o for do C. E, ao contrário do que parece, esse código não acaba ficando lento, nem consome muita memória por ser uma recursão, pois, graças a uma técnica chamada de tail recursion, a função recursiva é executada como se fosse iterativa.

Também tem a beleza matemática: a função em Scheme segue exatamente a definição por indução finita! Eu convido o leitor para tentar definir a soma de n números inteiros usando indução para então se convencer que a maneira com que Scheme trabalha é realmente natural, pelo menos natural para os iniciados na linguagem matemática.

O que eu acho mais fantástico em Scheme é a possibilidade de criar funções durante o período de execução e sem a necessidade de ter que dar nomes para as funções. A vantagem de não precisar das nomes é que não precisamos dar nomes para funções que não merecem um nome! :D

Em Scheme a função soma poderia ser chamada assim:

(soma 1 10 (lambda (x) (* x (+ x 1))))

Isso aí chama a função soma passando uma função que recebe x e devolde x*(x + 1). Bem divertido, hein? Scheme coloca em evidência a criação de funções em tempo de execução e acho que essa é justamente a graça do Scheme. Tem muito mais coisas legais que podem ser feitas com essa brincadeira de criar funções em tempo de execução: dá para usar funções para representar pares ordenados e até listas encadeadas. Você consegue fazer isso? Aliás, acho que a maior graça das linguagens funcionais é a separação entre o conceito de "dado" e "função" acaba não fazendo mais muito sentido.

Bem, eu tinha citado Ruby como exemplo de linguagem interessante para escrever o somatório, então vamos lá. Mas antes, deixa eu só eu fazer um comentário: um monte de gente fala que Ruby é uma linguagem que suporta o paradigma funcional e isso ainda me parece meio estranho. Em Ruby, apesar de ser possível criar métodos em tempo de execução, as funções não são normalmente tratadas como dados. O código a seguir, por exemplo, não funciona:

def soma(ini, fim, func)
    r = 0
    for i in (ini...(fim + 1))
        r = r + func(i)
    end
end

Existe uma maneira de passar funções como parâmetros, mas eu acho feio e tão antipático quanto passar funções em C usando ponteiros. Matz, o criador do Ruby, diz ter um bom motivo para escolher como os blocos e closures funcionam em Ruby, mas não gostei muito não. Talvez eu não tenha entendido. :)

Acho que muita gente concorda comigo, tanto que há um incentivo para que funções não sejam passadas como argumentos, mas sim que blocos de procedimentos sejam criados em tempo de execução e que esses blocos sejam passados como argumentos. Na verdade passar blocos é passar uma função como argumento mas... Ahn, ficou confuso? O código fica assim:

def soma(ini, fim)
  r = 0
  for i in (ini...(fim + 1))
    r = r + yield(i)
  end
  r
end

Aqui yield representa o bloco passado como argumento e a maneira de chamar a função soma é:

soma(1, 10) do |i|
  func(i)
end

Mas eu desconfio que um rubysta faria a coisa confusa:

r = 0
(1...11).each do |i|
  r = r + i*i
end

Talvez essa brincadeira de passar blocos e criar funções em tempo de execução que seja a parte dita funcional de Ruby, mas ainda não sei. Se você tiver alguns exemplos de práticas bem funcionais em Ruby ou Python, por favor, participe nos comentários.

Algo muito parecido pode ser feito com Python, fica como exercício para o leitor implementar e se divertir. =)

Espero que essas comparações entre as linguagens tenham sido interessantes e tenha acrescentado algo na vida dos leitores. Quem sabe não consegui ajudar algum iniciante a escolher qual linguagem aprender primeiro ou até mesmo a um programador mais experiente a aprender a olhar outras linguagens para descobrir outras maneiras de resolver os problemas que aparecem no dia a dia.

Sintam-se livres para criticar o código acima, sugerir outras implementações ou até mesmo mostrar como vocês fariam o somatório na sua linguagem preferida. Ah! Fico devendo uma cerveja para o primeiro que mostrar a implementação do somatório em brainfuck!