Kumpera.net delírios sobre linguagens de programação

Meus achados sobre tecnologia

Erlang é realmente difícil?

January 19th, 2008 · 6 Comments

Conversando com um amigo sobre Erlang, ele me comentou que acha a sua sintaxe quase indecifrável, note que se trata de um ótimo programador. Continuando a discussão, resolvi ver qual a diferença de algoritmos simples. Para tornar a comparação justa, vou mostrar o mesmo trecho de código em Java e Erlang. Meu objetivo é explorar quais são os maiores obstáculos de adoção eaprendizado. Começo com algo bem simples, somar todos os elementos de uma seqüência de inteiros:

Em Java:

int soma_array (int[] array) {
    int r = 0;
    for(int i = 0; i < array.length; ++i)
        r += array[i];
    return r;
}

Em Erlang:

soma_lista (Lista) ->
    soma_lista (Lista, 0).
 
soma_lista ([H | T], Acc) ->
    soma_lista (T, H + Acc);
 
soma_lista ([], Acc) ->
    Acc.

Nada de especial na versão em Java, já a versão em Erlang é bem enigmática, para a maioria quase indecifrável. O problema, talvez, seja o fato de ser uma linguagem puramente funcional, que significa, entre outras coisas, que não existe destructive assignment - não é possível alterar o valor de qualquer coisa depois de definida. Por isso que todo loop deve ser escrito usando tail recursion*.

Há quem possa achar que a sintaxe não ajuda, pois estamos usando notação própria para pattern matching e processamento de listas. Porém, como veremos, é possível reescrevê-lo removendo a sintaxe enxuta por algo mais familiar a linguagens imperativas. Para exemplificar esse processo, vou aproximar a versão Java o máximo da em Erlang e o primeiro passo é introduzir recursão.

int soma_array(int[] array) {
    return soma_array (array, 0, 0);
}
 
int soma_array(int[] array, int idx, int acc) {
    if (idx == array.length)
        return acc;
    return soma_array(array, idx + 1, acc + array[idx]);
}

Bem melhor agora! O código lembra um pouco mais a versão funcional. Porém ainda tem a diferença que Erlang usa listas encadeadas no lugar de arrays e são processadas maneira similar ao LISP, utilizando uma função que retorna o primeiro elemento e outra que retorna o resto da lista depois do primeiro. Se modificarmos o código Java para levar isso em conta, vamos ter algo que lembra muito Erlang. O código fica da seguinte forma:

interface Lista {
    int head();
    List tail();
    boolean isEmpty();
}
 
int soma_lista(Lista list) {
    return soma_array (list, 0);
}
 
int soma_lista(List list, int acc) {
    if (list.isEmpty())
        return acc;
    return soma_lista (list.tail(), acc + list.head());
}

Agora que temos uma versão do código Java com o devido processamento de listas, que tal reescrever a versão em Erlang para não usar a notação de processamento de listas e pattern matching. Com isso teremos algo que é gritantemente próximo ao exemplo anterior, como veremos a seguir:

soma_lista(Lista) ->
    soma_lista (Lista, 0).
 
soma_lista(Lista, Acc) ->
    if
        Lista == [] -> Acc;
        true -> soma_lista(tl(Lista), Acc + hd(Lista))
    end.

Agora sim, temos aquilo que meu amigo pode chamar de código decifrável. Porém não é sem abrir mão do poder da linguagem - o que é uma pena, eu diria. Um programador de Erlang provavelmente iria estranhar ver algo como escrito na última versão. Para terminar esse artigo vou mostrar como ficaria se utilizarmos as funções de acesso aleatorio a listas, algo não muito aconselhável, já que é uma operação O(N).

soma_lista(Lista) ->
    soma_lista(Lista, 1, 0).
 
soma_lista(Lista, Idx, Acc) ->
    if
        Idx > length(Lista) -> Acc;
        true -> soma_lista (Lista, Idx + 1, Acc + lists:nth(Idx, List))
    end.

*Já me descobri péssimo com tradução, por isso não me arriscarei com essa também.

Tags: Programming · erlang · java

6 responses so far ↓

  • 1 Proteu Alcebidiano // Jan 20, 2008 at 4:41 pm

    Legal o post, Rodrigo. Duas coisas:

    - Erlang não é puramente funcional, tem acesso a IO e sockets;

    - Quanto à funções com tail-recursion, eu arrisco traduzir como “Funções com recursão no final”.

    T+

  • 2 kumpera // Jan 20, 2008 at 11:16 pm

    A questão de ser puramente funcional é curiosa, pois depende de qual definição é usada.

    A mais comum é a que nenhum efeito colateral é possivel. Nesse caso é preciso abrir mão de construções avançadas para simplesmente fazer I/O.

    A outra visão é de que basta a linguagem computar tudo via aplicação de funções. O que exclui a existencia de side-effects internalmente a aplicação, porém não exclui efeitos externos.

    Sinceramente, Erlang pode ser vista como uma versão pragmática, pois, para efeitos práticos, também é possivel escrever em um arquivo usando Haskell e não precisamos nos dobrar todos com monads.

  • 3 Proteu Alcebidiano // Jan 21, 2008 at 1:08 am

    Pois então, estou me atendo ao conceito de side-effects, é o tipo de erro que é mais recorrente dos programadores em linguagens que aceitam updates (e que testes ajudam a encontrar quando estes falham).

    No caso de IO’s, Um registro em arquivo pode ser atualizado em situações práticas, é um side-effect inevitável, com ou sem rastreabilidade dos updates da informação.

    T+

  • 4 Andrei // Jan 22, 2008 at 3:08 pm

    Quando um programador diz que a “sintaxe da linguagem X é indecifrável”, geralmente significa que ele não está acostumado com aquela sintaxe. Até porque quem só aprendeu C e derivados na vida só viu uma forma de tratar a sintaxe de uma linguagem. O que não significa que não existam linguagens mais e menos legíveis, claro. Perl é um exemplo de linguagem considerada write-only, mas Erlang está muito longe disso.

    Quanto à pureza, é meio sem sentido dizer que uma linguagem é pura se não permite efeitos colaterais, já que não dá para fazer muita coisa sem eles. No contexto da programação funcional, uma linguagem é pura se ela possui transparência referencial. Haskell faz I/O e outros efeitos em mônadas, que são valores, e desde que as leis de mônadas sejam respeitadas, é possível garantir que a transparência referencial é mantida.

  • 5 Ricardo Herrmann // Jan 23, 2008 at 2:07 pm

    @Andrei: Só esclarecendo melhor o que você disse sobre pureza vs. efeitos colaterais.

    É possível ver I/O em Haskell como o acoplamento de dois elementos: (i) uma computação pura que gera um “programa” (um valor da mônada IO) e (ii) um executor (RTS), que executa as ações (I/O) desse programa, obtendo-as através de avaliação tardia (senão todas as ramificações do programa iram ser executadas para todas as entradas, o que não faz sentido). A transparência referencial vale na parte pura, com as mônadas efetuando o controle explícito dos efeitos colaterais causados pelo executor. Isso torna possível usar todo o ferramental de raciocínio equacional sobre funções que “fazem” I/O encapsuladas em mônadas, como se fossem um tipo qualquer, já que quem vai fazer o I/O propriamente dito é o executor.

    Ou será que eu só piorei a coisa ? ;-)

  • 6 Ricardo Herrmann // Jan 23, 2008 at 2:14 pm

    Aliás, já que tocaram no assunto de tradução (e eu acho que abusei disso também), “recursão de cauda” é o termo mais utilizado.

Leave a Comment