Nú

Números em Julia

7 exercícios

Sobre Números

O Julia é uma linguagem de uso geral, que pode ser usada na maioria das tarefas de programação. Na prática, porém, os principais casos de uso tendem a estar na engenharia e na ciência. Cálculos numéricos rápidos, versáteis e sofisticados estão no centro da sua conceção.

Números inteiros

Um número inteiro é um número "redondo", sem parte decimal.

No conceito Básicos, vimos que um valor inteiro pode ser atribuído a uma variável sem especificar um tipo.

Por questões de legibilidade, podes usar sublinhados como separador de algarismos. O compilador ignora-os.

julia> x = 3
3

julia> typeof(x)
Int64

julia> large_number = 1_234_567_890
1234567890

Internamente, o compilador usa o tipo de inteiro com sinal mais adequado ao teu CPU. Nos PCs modernos, este será habitualmente Int64, que é perfeitamente adequado à maioria das tarefas.

Os tipos serão discutidos com mais detalhe num conceito futuro. Para já, esperemos que os tipos numéricos sejam suficientemente intuitivos para que este documento faça sentido.

Como veremos num conceito mais adiante, o Julia suporta nativamente arrays muito grandes e multidimensionais. Estes podem pôr à prova tanto a quantidade de memória disponível como a largura de banda para transferências de dados.

Para dar ao programador um controlo mais fino, é possível e bastante comum especificar o tipo de inteiro, com ou sem sinal, pretendido. Muitos estudantes de Julia que já usaram Python podem querer comparar isto com o NumPy, em vez do Python básico.

julia> y = Int8(42)
42

julia> typeof(y)
Int8

julia> z = UInt32(1024)
0x00000400

Repara que, por omissão, o Julia mostra os inteiros sem sinal em formato hexadecimal, porque os tipos sem sinal são usados frequentemente em operações de bits de baixo nível.

Também podes escrever inteiros em binário, octal ou hexadecimal, com os prefixos 0b, 0o e 0x, respetivamente.

julia> a = 0x10
0x10

julia> Int(a)
16

julia> b = 0b1010 # displays as hexadecimal
0x0a

Transbordo de inteiros e BigInt

Cada tipo de inteiro tem um valor máximo e um valor mínimo que consegue armazenar:

julia> typemax(Int8)
127

julia> typemin(Int8)
-128

Sair deste intervalo válido provoca transbordo de inteiros, com resultados que parecem muito estranhos.

julia> c = Int8(126)
126

julia> c * c
4

Provavelmente concordamos que 126 * 126 não deve ser igual a 4!

Uma explicação detalhada exige conhecimentos sobre a forma como os registos do CPU armazenam inteiros, o que está fora do âmbito deste conceito.

O problema pode ser minimizado usando um tipo "mais largo", até Int128 ou UInt128 (o significado de "mais largo" é discutido abaixo). No entanto, todos têm limites rígidos antes de se depararem com problemas de transbordo.

Uma solução é usar o tipo BigInt, que só é limitado pela memória do teu computador. É a implementação do Julia de aritmética de precisão arbitrária.

julia> 2 ^ 70
0

julia> big = BigInt(2) ^ 70
1180591620717411303424

julia> UInt128(big)
0x00000000000000400000000000000000

Como 2 elevado a 70 não cabe num Int64, o primeiro cálculo do exemplo acima falha. Repara, no entanto, que não há qualquer mensagem de erro. Programador, tem cuidado!

A conversão de tipo de 2 num BigInt dá a resposta correta, e mostrá-lo em hexadecimal deixa mais claro que se trata de uma potência de 2.

Números de vírgula flutuante

Não será surpresa que os números de vírgula flutuante tenham, opcionalmente, um ponto decimal e uma parte fracionária depois do ponto.

julia> f = 3.45
3.45

julia> typeof(f)
Float64

Claro que a notação científica é suportada.

julia> avogadro = 6.02e23
6.02e23

Tal como acontece com os inteiros, o tipo predefinido serve para a maioria dos fins, mas existem outros tipos com sinal disponíveis. Não existem tipos de vírgula flutuante sem sinal.

Como atalho, podes criar valores Float32 usando um sufixo f0.

julia> f32 = 4.56f0
4.56f0

julia> typeof(f32)
Float32

Outros tamanhos de número de vírgula flutuante precisam de uma conversão de tipo explícita, tal como acontece com os inteiros.

Os valores máximo e mínimo podem ser uma surpresa:

julia> typemax(Float64)
Inf

julia> typemin(Float64)
-Inf

O infinito é um valor válido!

No entanto, o intervalo útil dos números de vírgula flutuante é limitado: tudo o que for muito grande recebe simplesmente o valor Inf, e tudo o que for muito pequeno é arredondado para 0.0.

Podemos usar outro par de funções para ver estes limites: aproximadamente ± 10 ^ 308.

julia> floatmax(Float64)
1.7976931348623157e308

julia> floatmin(Float64)
2.2250738585072014e-308

Além disso, a precisão é limitada, com um número grande mas finito de algarismos significativos representados (aproximadamente 15 para Float64).

Operadores aritméticos

Como discutimos no conceito Básicos, os operadores aritméticos funcionam sobretudo da mesma forma que a aritmética normal, tal como a ensinam às crianças. Repara que a exponenciação usa ^, e não ** (ambos são comuns noutras linguagens).

2 + 3  # 5 (addition)
2 - 3  # -1 (subtraction)
2 * 3  # 6 (multiplication)
8 / 2  # 4.0 (division)
8 % 3  # 2 (remainder)
2 ^ 3  # 8 (exponentiation)

No entanto, vale a pena discutir alguns detalhes específicos do Julia.

Multiplicação

julia> x = 4.2
4.2

julia> 2 * x
8.4

julia> 2x
8.4

julia> 2.4x
10.08

Isso pode ser surpreendente.

É sempre possível usar * como operador infixo, tal como na maioria das outras linguagens de programação.

No entanto, o Julia foi criado por pessoas que acreditam que o código deve parecer-se o mais possível com equações matemáticas.

Como os nomes de variáveis têm de começar por uma letra, colocar um número (inteiro ou de vírgula flutuante) antes do nome é tratado como multiplicação implícita.

Por exemplo, se quisermos a área da superfície de uma esfera, em vez de 4 * pi * r * r podemos fazer isto:

julia> surface(r) = 4π * r^2
surface (generic function with 1 method)

julia> surface(3)
113.09733552923255

Embora π seja uma constante incorporada, também é uma letra (grega). Por isso, o analisador sintático continua a precisar de um * explícito para separar π de r.

Divisão

Usar / como operador infixo dá sempre um resultado de vírgula flutuante, mesmo quando os valores de entrada são inteiros.

Para a divisão inteira, há mais opções:

julia> 10 / 3
3.3333333333333335

julia> div(10, 3)
3

julia> 10 ÷ 3
3

julia> 10 // 3
10//3

A função div() serve para a divisão inteira, com o resultado truncado em direção ao zero: para baixo nos números positivos, para cima nos números negativos.

Como sinónimo, podemos usar o operador infixo ÷, mais uma vez com o objetivo de o tornar mais matemático. Se estiveres a usar um editor que reconhece Julia, escreve \div e prime a tecla <Tab>.

O operador // precisará de um conceito próprio, mais adiante no programa.

Para já, basta dizer que o resultado de // é um número "racional", o nome formal daquilo a que a maioria das pessoas chama fração.

Os fatores comuns são removidos do numerador e do denominador, para dar uma razão entre dois inteiros naquilo a que se chama os "termos mais simples".

julia> rationalnum = 22 // 6
11//3

julia> typeof(rationalnum)
Rational{Int64}

Temos números racionais. E os números "irracionais"?

julia> π
π = 3.1415926535897...

julia> typeof(π)
Irrational{:π}

Um número irracional é aquele que não pode ser reduzido a uma razão de inteiros. Exemplos comuns incluem π, e (número de Euler) e muitas raízes, como √2 (a raiz quadrada de 2).

O Julia tenta fazer matemática como deve ser.

Conversão de tipos numéricos

Isto pode acontecer muitas vezes automaticamente:

julia> x = 2 + 3.5
5.5

julia> typeof(x)
Float64

Somámos um Int64 a um Float64 e obtivemos um resultado Float64.

Na verdade, o inteiro foi convertido silenciosamente para Float64 antes de se fazer a adição.

O Julia tem um conceito de "largura" dos tipos numéricos.

  • Dentro dos inteiros, e dentro dos números de vírgula flutuante, é apenas o número de bits necessários para o armazenamento. Assim, Int64 é mais largo do que Int16.
  • Os números de vírgula flutuante são considerados mais largos do que os inteiros, porque podem armazenar qualquer parte fracionária.

Se usarmos uma mistura de tipos numa expressão, cada um é "promovido" conforme necessário até ao tipo mais largo utilizado.

Para forçar a conversão, podemos converter um inteiro para um tipo específico, como em Float64(5).

Em alternativa, usa simplesmente float(5) e deixa o compilador escolher um tipo adequado.

As conversões de vírgula flutuante para inteiro são inevitavelmente mais complicadas. O que queres fazer com o que vem depois do ponto decimal?

  • A função round() converte para o número inteiro mais próximo, com os empates, como 4.5, a arredondar para o número inteiro par mais próximo.
  • floor() arredonda para baixo, ceil() arredonda para cima, trunc() arredonda em direção ao zero.
  • Tentar converter diretamente, por exemplo com Int32(), falha com um InexactError.

No entanto, por omissão, estas funções não devolvem o tipo de inteiro que poderias querer. É possível especificar o tipo de saída pretendido.

julia> round(4.5)
4.0

julia> round(Int64, 4.5)
4

julia> round(Int, 4.5)  # => default integer type
4

julia> ceil(Int16, 4.3)
5

Também é possível arredondar para um número especificado de algarismos depois do ponto decimal com o argumento nomeado digits.

julia> round(π, digits=10)
3.1415926536

Consulta o manual para mais detalhes.

Divisão por zero

Certamente isto dá simplesmente um erro? Na verdade, a situação não é assim tão simples.

A divisão inteira com ÷ ou // resulta num erro, como seria de esperar.

A divisão de vírgula flutuante com / segue aquilo a que se pode chamar uma abordagem de engenharia, em vez de uma abordagem padrão da informática:

julia> 2 / 0
Inf

julia> 0 / 0
NaN

Como discutimos numa secção anterior, o infinito é um número de vírgula flutuante válido no Julia, representado por Inf.

Quando o numerador também é zero, o resultado é matematicamente indefinido. O Julia trata-o então como um "não é um número", representado por NaN.

Se isto te parecer estranho, pensa nisto no contexto de trabalhar com arrays grandes de dados do mundo real (e, por isso, muitas vezes bastante desorganizados). Para avançar, o melhor é simplesmente assinalar os valores problemáticos e seguir em frente.

Uma verificação manual sem fim dos valores seria tediosa de programar e prejudicaria certamente o desempenho em tempo de execução.

Parar com uma mensagem de erro a cada pequena falha tornaria o teu programa muito pouco popular entre os utilizadores!

Comparar valores de vírgula flutuante

Como descrito no conceito Conditionals, os testes de igualdade fazem-se habitualmente com o operador ==.

Isto funciona bem para inteiros, carateres, strings, etc. No entanto, os valores de vírgula flutuante têm precisão limitada, e diferentes formas de calcular o mesmo resultado podem originar valores ligeiramente diferentes. Para Float64, isto andará tipicamente à volta do 15.º algarismo significativo: uma diferença pequena, mas não uma "igualdade".

Tradicionalmente, o conselho dado aos programadores é nunca usar == com valores de vírgula flutuante: os resultados são imprevisíveis.

Uma alternativa amplamente utilizada é testar o valor absoluto da diferença em relação a uma tolerância permitida (muitas vezes chamada épsilon ou ϵ). Por isso, em vez de a == b, usa abs(a - b) < epsilon.

O Julia oferece uma alternativa mais limpa com a função isapprox().

Para uma tolerância absoluta, a sintaxe é isapprox(a, b, atol=epsilon). O argumento nomeado atol é obrigatório neste caso.

A tolerância relativa é muitas vezes mais útil. É a predefinição, por isso isapprox(a, b) tentará escolher um valor sensato para rtol (as regras são bastante complicadas).

Também é possível especificar uma tolerância relativa, como uma fração dos valores que estão a ser comparados. Assim, isapprox(a, b, rtol=0.01 verifica se os valores estão a menos de 1% um do outro.

À boa maneira do Julia, existe um operador matemático sinónimo do caso predefinido: a ≈ b (com o operador escrito como \approx e depois <tab>).

Conceitos futuros relacionados

Além dos números racionais, partes mais adiante do programa abordarão:

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Números

A prática está bloqueada

Desbloqueia mais 2 exercícios para praticares Números