A Torre de Babel
Bruegel o Velho
c. 1563
Neste artigo proponho-me relatar a experiência na utilização de uma ferramenta de IA, no caso o Copilot (via Bing), na tradução de um pedaço de código Python para Erlang.
Desde logo convém realçar o desafio, que não se trata apenas de uma tradução gramatical entre duas linguagens mas, mais do que isso, uma tradução semântica entre linguagens de paradigmas totalmente distintos, pelo menos na utilização que aqui lhes dou:
Introdução
Imutabilidade
Esta é uma característica típica de linguagens funcionais (desde logo a Erlang mas também Haskell, Scala, F#, Clojure) e que permite, por exemplo, eliminar do horizonte muitos efeitos secundários perniciosos que advêm da partilha de memória (conteúdos de variáveis) em sistemas com concorrência.
Ao primeiro contacto, apresenta-se como uma limitação ao programador que venha de uma linguagem, normalmente procedimental ou orientada a objectos, em que o conteúdo de uma variável pode ser livremente alterado.
Estruturas como ciclos e acumulações, parecem impossíveis de resolver.
Um exemplo de um algoritmo trivial de cálculo do valor médio de um vector de números em C.
#include <stdio.h>
int soma(int vector[], int size) {
int total = 0;
// ALERTA MUTABILIDADE i++
for(int i=0; i<size; i++) { //
// ALERTA MUTABILIDADE total +=
total += vector[i];
}
return total;
}
double media(int vector[], int size) {
return soma(vector, size) / (size + 0.0);
}
int main()
{
int valores[] = {10, 12, 15, 30, 50, 12, 45};
printf("Media: %f", media(valores, 7));
return 0;
}
Media: 24.857143
...Program finished with exit code 0
Press ENTER to exit console.
Ou uma versão equivalente em Python, um pouco menos mutável mas não totalmente:
def soma(vector):
total = 0
for valor in vector:
# ALERTA MUTABILIDADE total +=
total += valor
return total
def media(vector):
return soma(vector) / len(vector)
vector = [10, 12, 15, 30, 50, 12, 45]
print(media(vector))
PS .\Source\Python\ProjectEuler\media> python.exe .\media.py
24.857142857142858
Perante problemas deste tipo, as linguagens funcionais com imutabilidade de variáveis fazem uso, extensivo, da recursividade.
Exemplificando o mesmo problema em Erlang, mas antes alguma semântica básica introdutória para os menos versados.
[H|T] - Lista em que H , head, cabeça, é o primeiro elemento da lista e T uma lista com os elementos seguintes.
[_|T] - Lista em que não queremos saber do primeiro elemento para nada.
[H|[]] - Lista composta por um elemento, no caso H sendo o resto a lista vazia [].
Pattern matching
Em Erlang, uma função pode ter várias "diferentes implementações" sendo a sua selecção e utilização dependente dos argumentos fornecidos.
Cada um dos casos será inspeccionado e o primeiro em que os argumentos fornecidos correspondam à definição será utilizado.
Exemplo:
len([_|[]], Acc) será utilizada sempre que a lista fornecida tenha apenas um elemento, caso contrário será utilizada len([_|T], Acc).
-module(media).
-export([soma/2, len/2, media/1, run/1]).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%% Tamanho de uma lista %%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% caso base
len([_|[]], Acc) ->
Acc + 1;
% caso geral
len([_|T], Acc) ->
% recursão extraindo o primeiro valor da lista
len(T, Acc + 1).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%% Soma de uma lista %%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% caso base
soma([H|[]], Acc) ->
H + Acc;
% caso geral
soma([H|T], Acc) ->
% recursão usando e extraindo o primeiro valor da lista
soma(T, H + Acc).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Média dos valores de uma lista %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
media(Vector) ->
soma(Vector, 0) / len(Vector, 0).
run(Vector) ->
io:format("Media: ~w~n", [media(Vector)]).
Erlang/OTP 26 [erts-14.1.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V14.1.1 (press Ctrl+G to abort, type help(). for help)
1> c(media).
{ok,media}
2> media:run([10, 12, 15, 30, 50, 12, 45]).
Media: 24.857142857142858
ok
3>
O caso
Quem gosta de linguagens de programação (e de programar) está sempre à procura de desafios, e um local salvífico do tédio do programador é o Project Euler (vão ver).
Um problema que num daqueles momentos resolvi tentar resolver é o problema 31 e consiste em calcular quantas maneiras diferentes há de perfazer um determinado valor monetário tendo em conta os tipos de moedas e notas disponíveis.
No problema é dado o exemplo inglês mas pode, sem alteração, ser usado em Portugal com euros: completar 2 euros com moedas de 1, 2, 5, 10, 20 e 50 cêntimos e de 1 e 2 euros.
Segundo o portal, o problema foi solucionado 92203 vezes e é de dificuldade elementar (5%).
Há soluções em todas as linguagens, algumas lindíssimas e curiosíssimas, e eu tentei também a minha sorte com Pyhton.
Cá está ela:
import math
def calc(target, coins):
if len(coins) == 1:
return 1 if target % coins[0] == 0 else 0
total = 0
for i in range(len(coins)):
c = math.floor(target/coins[i])
while c > 0:
remaining = target - c * coins[i]
if remaining == 0:
total += 1
elif remaining > 0:
total += calc(remaining, coins[i+1:])
c -= 1
return total
coins = [200,100,50,20,10,5,2,1]
print(calc(200, coins))
O algoritmo é básico, poderá certamente ser melhorado, was not my point, mas despacha-se razoavelmente bem até um valor de teste de "bom senso". :)
Por exemplo:
- 1 cêntimo (1 maneira)
- 2 cêntimos (2 maneiras)
- 10 cêntimos (11)
- 2 euros (73682)
- 3 euros (471363)
- 5 euros (6295434) <- 5 segundos
- 6 euros (16960860) <- 11 segundos
E não vale a pena alargar mais o leque que o tempo é exponencial.
A "funcionalização" do algoritmo
Foi então que resolvi reescrever o mesmo algoritmo em Erlang, e com se ouve dizer que um das ajudas que a IA podia dar na programação é a de, precisamente, traduzir linguagens de programação, passei a palavra ao Copilot no Bing.
Solicito educadamente "Podes traduzir este pedaço de código Python para Erlang?" e replico o código fonte que mostrei acima.
Responde quase imediatamente, embora não pareça totalmente confiante: "Este código Erlang deve replicar a lógica do código Python que forneceste. Se tiveres alguma dúvida ou precisares de mais alguma coisa, estou aqui para ajudar!"
-module(coin_calc).
-export([calc/2]).
calc(Target, Coins) ->
case length(Coins) of
1 ->
if Target rem hd(Coins) =:= 0 -> 1;
true -> 0
end;
_ ->
calc(Target, Coins, 0)
end.
calc(_, [], Total) -> Total;
calc(Target, [Coin | Rest], Total) ->
C = Target div Coin,
calc(Target, Coin, Rest, Total, C).
calc(_, _, _, Total, 0) -> Total;
calc(Target, Coin, Rest, Total, C) ->
Remaining = Target - C * Coin,
NewTotal = if Remaining =:= 0 -> Total + 1;
Remaining > 0 -> Total + calc(Remaining, Rest);
true -> Total
end,
calc(Target, Coin, Rest, NewTotal, C - 1).
Vamos lá experimentar:
Erlang/OTP 26 [erts-14.1.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V14.1.1 (press Ctrl+G to abort, type help(). for help)
1> c(coin_calc).
{ok,coin_calc}
2> coin_calc:calc(200,[200,100,50,20,10,5,2,1]).
1
3>
Hmm! 1?
Voltou ao meu amigo: "Infelizmente não está a funcionar correctamente, devolve sempre o valor 1." ao que me responde solícito: "Entendi, vamos tentar corrigir isso. Aqui está uma versão ajustada do código Erlang. Nesta versão, adicionei uma função
calc_loop para iterar sobre as combinações de moedas e ajustar a contagem total. Tente este código e veja se ele resolve o problema. Se precisar de mais alguma coisa, estou aqui para ajudar!"-module(coin_calc).
-export([calc/2]).
calc(Target, Coins) ->
calc(Target, Coins, 0).
calc(0, _, Total) ->
Total + 1;
calc(_, [], Total) ->
Total;
calc(Target, [Coin | Rest], Total) ->
C = Target div Coin,
calc_loop(Target, Coin, Rest, Total, C).
calc_loop(_, _, _, Total, 0) ->
Total;
calc_loop(Target, Coin, Rest, Total, C) ->
Remaining = Target - C * Coin,
NewTotal = if Remaining =:= 0 -> Total + 1;
Remaining > 0 -> Total + calc(Remaining, Rest);
true -> Total
end,
calc_loop(Target, Coin, Rest, NewTotal, C - 1).
Não resolveu.
Insisti: "Mesmo resultado errado; sempre 1." ao que me respondeu com "Vamos tentar uma abordagem diferente para garantir que a lógica está correta. Aqui está uma versão revisada do código Erlang:" mas com o código exactamente igual ao anteriormente fornecido.
Fiz uma última tentativa, dando uma ajuda: "Não. O código é exactamente igual ao anterior. Parece-me que falta uma chamada recursiva algures." mas sem resultado.
Repetiu o código e eu deixei-o em paz.
Posso não estar a fazer as perguntas certas e pode o Copilot via Bing não ser ferramenta adequada (testei no ChatGPT 3.5 com resultados erradamente semelhantes; se alguém ler isto e quiser testar noutra plataforma, dê notícias, obrigado).
Algo ressalta, a meu ver, desta experiência.
Conclusão
A tradução efectuada está errada (abaixo apresento uma versão corrigida) e pude confirmar que estava errada apenas porque o resultado era óbvia e evidentemente errado: 1, sempre.
Imaginemos agora que o resultado era errado mas não desta forma gritante, só "ligeiramente". Que hipótese teria eu de o validar? Usar o código Python, que sei correcto, para validar respostas da versão Erlang? Imaginemos outro tipo de problemas com entradas e respostas mais complexas.
É impraticável.
E depois de confirmado o erro, como descobrir onde está o bug?
A menos que se saiba algo, bastante mais do que o trivial de Erlang, dificilmente se será capaz de tornar o código utilizável.
Posfácio
O código corrigido (a amarelo a alteração):
-module(coin_calc).
-export([calc/2]).
calc(Target, Coins) ->
calc(Target, Coins, 0).
calc(0, _, Total) ->
Total + 1;
calc(_, [], Total) ->
Total;
calc(Target, [Coin | Rest], Total) ->
C = Target div Coin,
calc_loop(Target, Coin, Rest, Total, C).
calc_loop(Target, _, Rest, Total, 0) ->
Total + calc(Target, Rest, 0);
calc_loop(Target, Coin, Rest, Total, C) ->
Remaining = Target - C * Coin,
NewTotal = if Remaining =:= 0 -> Total + 1;
Remaining > 0 -> Total + calc(Remaining, Rest);
true -> Total
end,
calc_loop(Target, Coin, Rest, NewTotal, C - 1).
Curiosidade: tempos de execução em Erlang aproximadamente 90% mais rápidos do que a versão em Python.
_-_Google_Art_Project_-_edited.jpg)
Sem comentários:
Enviar um comentário