Diferentes formas de expressar em código

Existem diferentes formas de se programar algo, mesmo quando usa-se a mesma linguagem de programação. Embora esses códigos possam ser considerados equivalentes, uma vez que apresentam o mesmo resultado, eles tem suas particularidades, podendo facilitar ou dificultar sua leitura e manutenção. Usando como exemplo a impressão dos valores de uma lista (ou array) no terminal utilizando a linguagem Rust, pretendo mostrar essas diferenças na prática, de forma que também possa ser abstraído para outras lógicas e linguagens.

Problema proposto

Dado uma lista:

let lista = [1, 6, 3, 8, 4, 3];

Deseja-se imprimir no terminal o índice e valor de cada elemento, conforme a baixo:

0 => 1
1 => 6
2 => 3
3 => 8
4 => 4
5 => 3

Uma forma de se resolver esse problema é iterando sobre os elementos da lista, imprimindo os índices e valores.

Código 1: Acesso direto

Uma primeira forma de resolver esse problema é acessando cada valor diretamente no código. Exemplo:

println!("{} => {}", 0, lista[0]);
println!("{} => {}", 1, lista[1]);
println!("{} => {}", 2, lista[2]);
println!("{} => {}", 3, lista[3]);
println!("{} => {}", 4, lista[4]);
println!("{} => {}", 5, lista[5]);

Esse código é simples e direto, tendo como resultado da sua execução o resultado desejado. Porém apresenta dois pontos principais: ele é fixo para 6 elementos, sendo necessário sua alteração caso a quantidade de elementos da lista seja alterado; também existe uma repetição no código, sendo necessário alterar todos os println!(...) caso deseja-se mudar a saída.

Código 2: while

É possível escrever o código de forma que ele se adapte a quantidade de elementos da lista, sendo necessário uma estrutura de repetição para isso. Exemplo:

let mut i = 0;
while i < lista.len() {
    println!("{} => {}", i, lista[i]);
    i += 1;
}

Esse código utiliza a estrutura de repetição while para melhorar os dois pontos destacados no código anterior, verificando o tamanho da lista em tempo de execução, e evitando a repetição do println!(...). Porém isso vem com o custo adicional de uma variável e alguns ciclos de processamento adicionais para fazer o controle da estrutura de repetição (acessar o tamanho da lista e compará-lo a variável de controle).

Código 3: for

Outra forma possível é a utilização da estrutura de repetição for. Exemplo:

for i in 0..lista.len() {
    println!("{} => {}", i, lista[i]);
}

Esse código, assim como o anterior, necessita de uma variável para o controle da estrutura de repetição. Entretanto, o controle desta variável fica a cargo da linguagem e não do programador, não sendo necessário incrementá-lo manualmente, e fica mais claro no código como a estrutura de repetição está sendo controlada. Um ponto negativo é que não é possível alterar essa variável diretamente, o que possibilitaria imprimir os valores em outra ordem, com alguma lógica mais elaborada, como seria possível no Código 2.

Código 4: Iterando com for

Em Rust é possível iterar diretamente os valores de uma lista utilizando o for. Exemplo:

for &v in lista.iter() {
    println!("_ => {}", v);
}

Embora esse seja o código mais simples usando uma estrutura de repetição, e fica claro no código de que a interação está ocorrendo em cima dos valores da lista, não é possível mostrar o índice do valor no println!(...).

Código 5: Iterando com for e .enumerate()

Para permitir que o índice seja mostrado, é possível enumerar os valores da lista. Desta forma, em vez de iterar nos elementos da lista diretamente, o for itera em tuplas com o valor dado pelo .enumerate() e o elemento da lista. Exemplo:

for (i, &v) in lista.iter().enumerate() {
    println!("{} => {}", i, v);
}

Esse código se assemelha ao anterior em simplificação, porém permitindo que o índice seja impresso no terminal. Embora ele seja bem parecido com o Código 3, seu for deixa claro que a estrutura de repetição está iterando sobre os valores da lista, enquanto o for do Código 3 apenas diz que está sendo iterado sobre seus índices, necessitando verificar o bloco de código do for para entender no que o índice está sendo usado, e necessitando acessar manualmente os valores da lista, o que poderia dificultar a alteração do nome da variável da lista, por exemplo, uma vez que o mesmo precisaria ser alterado em diversos lugares.

Código 6: .for_each()

Rust também permite que o código seja feito utilizando funções em vez de uma estrutura de repetição (embora a estrutura de repetição seja usada internamente por essas funções). Para esse caso foi utilizado a função .for_each() do iterador, que permite chamar uma função passada por argumento para cada valor da lista. Exemplo:

lista
    .iter()
    .enumerate()
    .for_each(|(i, &v)| println!("{} => {}", i, v));

Esse código está dividido em várias linhas apenas para facilitar sua leitura, podendo estar em uma única linha. O compilador do Rust também consegue otimizar esse código deixando-o mais performático que a versão utilizando o for. Porém a versão com for deixa explícito no código a iteração dos elementos da lista, o que poderia facilitar a sua leitura. Nesse código também não é possível utilizar comandos como continue e break para controlar a estrutura de repetição, necessitando utilizar outras funções do iterador para comportamentos distintos.

Considerações

Com exceção do Código 4, todos os outros têm o mesmo resultado no terminal, sendo opções de código viáveis de resolução do problema proposto. Cada código possui suas diferenças, variando em legibilidade e flexibilidade. Pensando em percorrer os elementos da lista para resolver o problema, essa lógica pode ser expressa de diferentes formas, como visto nos diferentes códigos apresentados, assim como em português é possível transmitir uma mesma ideia com diferentes frases.

Abstraindo para um sistema, existem diferentes formas de resolver as pequenas partes do sistema, e muitas vezes essas pequenas partes podem apresentar similaridades. Quando essas partes são resolvidas utilizando a mesma técnica, isso pode facilitar a leitura e manutenção do código, uma vez que fica mais fácil de pressupor o que o código está fazendo conforme o programador se habitua com ele. Assim como, ao se terminar uma alteração, é possível fazer uma análise, verificando se não existe uma melhor forma de expressar a lógica já implementada, deixando o código mais organizado, o que facilitaria a leitura do mesmo.