Ruby

O fantástico (e perigoso) mundo dos números em Ruby

09 Aug 2022

Assim como todas as linguagens de programação, Ruby tem seus tipos numéricos. O mais simples de todos é do tipo integer (1, 5, 60). Também temos floats (1.3, 5.67), rationals (10/2r) e BigDecimal (0.1e1) que não faz parte das classes padrão e precisa ser carregada no código. A princípio, usamos números inteiros sempre que possível, a não ser que você queira lidar com números fracionados.

Uma característica interessante dos Integers em Ruby, que acaba sempre chamando atenção e é uma boa forma de gerar um "wow" em pessoas que ainda não conhecem a linguagem, é a capacidade de chamar o método #times em um número e ele iterar pelo código automaticamente, sem nenhum for loop:

10.times do
  # isso vai ser rodado 10 vezes
end

Mas, apesar desta beleza, existem alguns perigos ao usar números na programação que precisam ser levados em consideração.

Cuidado com a divisão de inteiros

Ruby se comporta ao dividir os inteiros retornando apenas o quociente e ignorando o resto, o que pode gerar muita confusão:

7 / 3
=> 2

A solução para conseguir um valor mais exato é não usar dois números inteiros.

7.0 / 3 
=> 2.3333333333333335

Mas isso pode nos levar a outro perigo:

Cuidado com a precisão dos floats

Recentemente houve um problema no app da Nubank que muitos apontaram o uso indevido do float como causa. Os usuários não conseguiam fazer pix com o valor de 17.99, sendo truncada para 17.98. O erro envolve um problema conhecido do uso de floats para representar dinheiro ou outros números que precisam de uma grande precisão.

Vários usuários relataram o problema no Twitter.

E esse erro é mais fácil de acontecer do que parece. Quando executamos 1.2 - 1.0 no Ruby, esperamos o resultado 0.2, mas recebemos um 0.19999999999999996. Como isso acontece?

Os números em um código, como você deve saber, não são "compreendidos" pelo computador. A linguagem da máquina é binário, sendo representado por 0 e 1 (não os números). Então quando digitamos 1.9 no Ruby, esse número é convertido para binário para aí sim a operação ser realizada. Nesse processo de conversão, nem todos os números fracionados podem ser armazenados em binário com grande precisão. E isso acontece não só na programação, pois existem números assim na matemática decimal também, como 10 / 3 = 3.33333.....

Qual é a solução, então? Uma delas é usar BigDecimal em Ruby, que gera muito mais precisão. Só que BigDecimal não é conhecido por ser o mais performático ou mesmo simples de lidar. Eu tentei usar em um projeto e acabei ficando perdido.

É possível "salvar" o float usando métodos apropriados para lidar com suas imperfeições. Uma das hipóteses para o que aconteceu no Nubank era que eles estavam truncando o número, em vez de arrendondá-lo. Em Ruby, temos o método truncate() que vai "cortar" o número com a precisão especificada, mas o ideal é usar o método round() que vai arredondar o número para o mais próximo de acordo com a precisão especificada.

0.19999999999999996.truncate(1) # => 0.1
0.19999999999999996.round(1) # => 0.2

Pronto, resolvemos o nosso problema.

A solução "racional"

Segundo Jeremy Evans no livro Polished Ruby Programming, a melhor forma de lidar com números é:

"Um bom princípio geral é usar racionais sempre que precisar fazer cálculos com valores não inteiros e querer valores exatos. Para casos em que exatidão não é importante ou você está apenas fazendo comparações com números e não cálculos que acabam acumulando erros, é provavelmente melhor usar floats." (p. 10)

Ele nos diz que racionais são mais ou menos 2 a 5 vezes mais lentos do que floats, mas que não precisam ser ignorados pois acaba sendo uma solução mais "rápida" do que ficar tratando esses números depois. No nosso exemplo dado acima:

1.2r - 1.0r # => (1/5)

Temos aqui um resultado com dois inteiros, um como numerador e outro como denominador, o que gera uma grande precisão, mas vamos combinar: é péssimo para ler. Para isso, convertemos para float e teremos um número mais fácil de entender:

(1.2r - 1.0r).to_f # => 0.2

Essa foi uma pequena viagem pelo incrível mundo dos números na programação. Espero que não os tenha assustado. Com um pouco de treino é possível domá-los aos nossos objetivos.

Receba atualizações por e-mail

Sempre que tiver novidades por aqui vou enviar aos inscritos.

Entre na conversa

Criei uma postagem lá no LinkedIn para receber comentários:

Comente