O caso do ‘if’ malvadão!

No Curso Básico de Programação do Bash, nós sempre falamos de coisas que chegam até nós como regras que, vindas das celebridades certas, são tomadas cegamente como verdades absolutas. Em boa parte, são ideias importantes reduzidas a clickbaits, como é o caso do "if malvadão".


O caso do 'if' malvadão

Este artigo foi inspirado pelas nossas conversas no Curso Básico de Programação do Bash, pela análise recente do script de um colega da nossa comunidade e foi baseado na palestra de Jules May (2015), que explica e ilustra muito bem o problema e as soluções propostas.

A história da estrutura if ser nociva é apenas um dos debates profundos sobre paradigmas e técnicas de programação que foram reduzidos a "click baits" e "tretas dev" que, frequentemente, passam longe da real preocupação do tema e do esforço para a elaboração de soluções.

O cerne da questão está no conflito entre o que está escrito no texto de um código e o significado do código em execução : o código é uma representação estática de algo que será executado dinamicamente ao longo de um determinado tempo. Em linguagens estruturadas, o problema começa quando, de algum modo, o código em execução viola a estrutura e impossibilita o acompanhamento histórico do fluxo de execução. Este problema foi originalmente atribuído à declaração "goto" por Edgar Dijkstra em um artigo de 1968. No artigo, ele diz:

"Por vários anos, me tem sido familiar a observação de que a qualidade dos programadores é uma função inversa da densidade de declarações go to nos programas que eles produzem."

Parando na primeira frase do artigo, poderíamos supor que se trata de uma das primeiras lacrações da história da programação. Mas os tempos eram outros e as pessoas ainda tinham o hábito de ir além das manchetes ou dos primeiros 240 caracteres de um artigo. Na sentença seguinte, ele continua:

"Mais recentemente, eu descobri por que o uso da declaração go to tem efeitos tão desastrosos e me convenci de que a declaração go to deve ser abolida de todas as linguagens de programação de alto nível." -- grifo nosso.

Na continuidade do artigo, ele demonstra o que nós já dissemos: o código é uma representação estática de de algo que será executado dinamicamente ao longo do tempo, e instruções como o goto podem ter o efeito de inviabilizar um acompanhamento do histórico do fluxo de execução. Ao longo dos debates subsequentes, notou-se que sub-rotinas, estruturas (inclusive loops e estruturas de decisão), além de instruções como 'return', 'break' e 'continue' (discutivelmente), todas comparáveis a "goto's", não violavam necessariamente a estrutura do código em execução -- aliás, nem o próprio goto era necessariamente um problema.

O problema, de fato, acontece quando o goto (ou qualquer estrutura ou subrotina) está subordinado a uma condição, por exemplo:

if CONDIÇÃO goto LINHA_N

E é aí que entra o 'if', introduzindo disfunções no histórico do fluxo de execução. Consequentemente, por mais que o código seja "limpo" e legível, na execução, ele acaba se tornando aquilo que nós chamamos de "espaguete". Portanto, se os problemas vêm com os "goto's" condicionados, ou até com "if's" condicionados, o problema não está no exatamente no goto ou no if, mas na forma como escrevemos as condições a que submetemos os "goto's" e "if's".

Mas o Bash não tem goto!

Pois é, não tem. Mesmo que tivesse, o impacto do problema é mais sentido em linguagens compiladas ou quando o código é submetido a um módulo de testes ou de debug. O Bash não tem goto nem é compilado, mas tem if e os scripts podem ser submetidos a testes: logo, a preocupação é real.

Imagine um código assim:

if ((var > 0)); then
    echo Maior
else
    echo Menor ou igual
fi

Não raro, um módulo de testes terá que implementar condições, talvez com um if, e nós teremos um if (no código) submetido a uma condição (no teste). Na esmagadora maioria dos casos, este não será um cenário aplicável, mas nós podemos nos beneficiar de técnicas alternativas que visam evitar a violação das estruturas em execução.

Soluções propostas

Existem algumas abordagens propostas para garantir a integridade das estruturas em execução...

Concentrar todas as consequências de uma decisão em um mesmo bloco

Não raro, nos deparamos com isso:

for var in {-10..10}; do
    if ((var > 0)); then
        echo Maior
    else
        echo Menor ou igual
    fi
    if ((var > 0)); then
        ((var--))
    else
        ((var++))
    fi
done

Aqui, a mesma condição ( ((var > 0)) ) é avaliada duas vezes no loop for. Não precisamos que o Bash tenha goto nem que seja uma linguagem compilada para ver que existe um problema aqui. Ele não vai se manifestar como um bug, mas como um problema de expressão de intenção: o que o programador quis dizer com isso?

A solução é simples:

for var in {-10..10}; do
    if ((var > 0)); then
        echo Maior
        ((var--))
    else
        echo Menor ou igual
        ((var++))
    fi
done

Mas podemos fazer algo ainda mais interessante...

Buscar tornar implícitas as decisões que geram essas consequências

Existem dois paradigmas relativos à forma como escrevemos as nossas instruções (ou comandos, no caso do Bash):

  • Paradigma imperativo: nós dizemos o que queremos que o computador faça.
  • Paradigma declarativo: nós expressamos uma intenção.

O Bash é uma linguagem estruturada onde as instruções são dadas de forma imperativa (daí serem chamadas de "comandos"). Na programação funcional, porém, o que se busca é declarar intenções, mas existem muitas construções que podem nos ajudar a, no mínimo, emular abordagens declarativas, o que é muito útil quando queremos tornar implícito o que seria obtido com uma estrutura condicional explícita.

Ainda no exemplo anterior, nós poderíamos fazer algo assim:

for var in {-10..10}; do
    # Se avaliada como verdadeira, esta linha não não será executada.
    # Caso contrário, as linhas seguintes é que não serão executadas.
    ((var > 0)) || { echo Menor ou igual; ((var++)); continue; }
    
    echo Maior
    ((var--))
done

Essa abordagem contempla, ao mesmo tempo, duas outras propostas para manter a integridade estrutural do código em execução:

  • Preferir interromper o fluxo a controlá-lo (o que fizemos com continue).
  • Preferir afirmar estados e testá-los numa mesma expressão (o que fizemos observando o estado de saída da expressão no comando composto (().

Lidar com tipos e valores com funções específicas

Nem sempre a avaliação de uma condição será feita a partir de uma comparação numérica. Aliás, até para avaliar uma expressão numérica, não será raro precisarmos avaliar se o dado em si é ou não numérico. O Bash fornece construções excelentes para este tipo de avaliação com o comando composto [[ (test do Bash) ou com suas formas POSIX nos comandos test e [.

Alternativamente, nós podemos implementar essas avaliações na forma de funções (o que, no Bash, pode ser uma simples questão de nomear um comando composto).

Observe:

if [[ $1 =~ ^[0-9]+ ]]; then
    printf 'a soma de %d com 10 é ' $1
    echo $(($1 + 10))
else
    echo "não é possível somar 10 a $1!"
    exit 1
fi

Aqui, nós poderíamos utilizar a abordagem anterior facilmente:

[[ $1 =~ ^[0-9]+ ]] || { echo "não é possível somar 10 a $1!"; exit 1; }
printf 'a soma de %d com 10 é ' $1
echo $(($1 + 10))

Mas também podemos deixar as avaliações e consequências para funções distintas:

soma10_int() {
    printf 'a soma de %d com 10 é ' $1
    echo $(($1 + 10))
}

soma10_str() {
    [[ $1 =~ ^[0-9]+ ]] && return 0
    echo "não é possível somar 10 a $1!"
    return 1
}

main() {
    soma10_str $1 || return 1
    soma10_int $1
}

main $1

Sim, escrevemos mais código, mas esse é o tipo de decisão que precisa ser balanceada de acordo com cada caso. Certamente, esta última abordagem garante a integridade da estrutura tanto no código quanto no fluxo de execução, sem falar em detalhes como legibilidade, expressão de intenção e facilidade de manutenção, mas não garante nem mais nem menos do que as demais abordagens apresentadas aqui.

Conclusão

A principal lição dessa história é que não devemos aceitar a imposição de regras sem compreender os reais problemas que, com toda certeza, foram reduzidos a simplificações que não acrescentam nada em termos de conhecimento nem de aprimoramento das nossas habilidades como programadoræs.

No fim das contas, ao menos no Bash, o if não é um comando a ser banido e muito menos será sempre prejudicial. O importante é conhecer como as coisas funcionam e saber explicar o porquê das decisões que tomamos nos nossos scripts e programas.

Melhor ainda: veja quantas técnicas interessantes nós descobrimos! Às vezes, é justamente quando somos desafiados a sair da zona de conforto que o desenvolvimento acontece (em todos os sentidos).

Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment