Nú

Números em Julia

7 exercícios

Sobre Números

Julia é uma linguagem de propósito 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 do seu projeto.

Números inteiros

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

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

Para facilitar a leitura, sublinhados podem ser usados como separador de dígitos. Eles são ignorados pelo compilador.

julia> x = 3
3

julia> typeof(x)
Int64

julia> large_number = 1_234_567_890
1234567890

Internamente, o compilador vai usar o tipo inteiro com sinal mais adequado para a sua CPU. Em PCs modernos, isso normalmente será Int64, que é perfeitamente adequado para a maioria das tarefas.

Tipos serão discutidos com mais profundidade em um conceito futuro. Por enquanto, os tipos numéricos devem ser intuitivos o bastante para que este documento faça sentido.

Como veremos em um conceito mais adiante, Julia dá suporte nativo a arrays muito grandes e multidimensionais. Eles podem sobrecarregar tanto a quantidade de memória disponível quanto a largura de banda para a transferência de dados.

Para dar mais controle ao desenvolvedor, é possível e bastante comum especificar o tipo inteiro com sinal ou sem sinal desejado. Muitos estudantes de Julia que já usaram Python podem querer comparar isso com o NumPy, em vez do Python básico.

julia> y = Int8(42)
42

julia> typeof(y)
Int8

julia> z = UInt32(1024)
0x00000400

Repare que Julia, por padrão, exibe inteiros sem sinal no formato hexadecimal, porque os tipos sem sinal são muito usados em operações de bits de baixo nível.

Também é possível escrever inteiros em binário, octal ou hexadecimal, com os prefixos 0b, 0o e 0x, respectivamente.

julia> a = 0x10
0x10

julia> Int(a)
16

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

Estouro de inteiros e BigInt

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

julia> typemax(Int8)
127

julia> typemin(Int8)
-128

Sair desse intervalo válido causa um estouro de inteiro, com resultados que parecem muito estranhos.

julia> c = Int8(126)
126

julia> c * c
4

Provavelmente podemos concordar que 126 * 126 não deveria ser igual a 4!

Uma explicação detalhada exigiria saber como os registradores da CPU armazenam inteiros, o que está fora do escopo 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 esbarrar em problemas de estouro.

Uma solução é usar o tipo BigInt, que é limitado apenas pela memória do seu computador. É a implementação de Julia da 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 em um Int64, o primeiro cálculo do exemplo acima falha. Repare, porém, que não há mensagem de erro. Atenção, programador!

Uma conversão de tipo de 2 para BigInt dá a resposta correta, e exibi-lo em hexadecimal deixa mais claro que é uma potência de 2.

Ponto flutuante

Não vai ser surpresa que números de ponto 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

Como acontece com os inteiros, o tipo padrão serve para a maioria dos casos, mas há outros tipos com sinal disponíveis. Não existem tipos de ponto flutuante sem sinal.

Como atalho, valores Float32 podem ser criados usando o sufixo f0.

julia> f32 = 4.56f0
4.56f0

julia> typeof(f32)
Float32

Outros tamanhos de ponto flutuante precisam de uma conversão de tipo explícita, como no caso dos inteiros.

Os valores máximo e mínimo podem surpreender:

julia> typemax(Float64)
Inf

julia> typemin(Float64)
-Inf

O infinito é um valor válido!

No entanto, o intervalo útil dos números de ponto flutuante é limitado: qualquer valor muito grande simplesmente recebe o valor Inf, e qualquer valor muito pequeno é arredondado para 0.0.

Podemos usar outro par de funções para ver esses 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 discutido no conceito Básico, os operadores aritméticos funcionam quase da mesma forma que a aritmética padrão ensinada às crianças. Repare que a exponenciação usa ^, e não ** (as duas formas são comuns em outras 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, alguns detalhes específicos de Julia vale a pena discutir.

Multiplicação

julia> x = 4.2
4.2

julia> 2 * x
8.4

julia> 2x
8.4

julia> 2.4x
10.08

Isso pode surpreender.

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

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

Como os nomes de variáveis precisam começar com uma letra, colocar um número (inteiro ou de ponto 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 poderíamos fazer isto :

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

julia> surface(3)
113.09733552923255

Embora π seja uma constante embutida, ela também é uma letra (grega). Por isso, o parser ainda precisa de um * explícito para separar π de r.

Divisão

Usar / como operador infixo sempre dá um resultado de ponto flutuante, mesmo com entradas inteiras.

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, e para cima, nos negativos.

Como sinônimo, podemos usar o operador infixo ÷, de novo com o objetivo de deixar tudo mais matemático. Se você estiver usando um editor que reconhece Julia, digite \div e depois aperte a tecla <Tab>.

O operador // vai precisar de um conceito próprio, mais adiante no currículo.

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

Fatores comuns serão removidos do numerador e do denominador, para dar uma razão de dois inteiros na chamada "forma irredutível".

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

julia> typeof(rationalnum)
Rational{Int64}

Temos os 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).

Julia procura fazer matemática do jeito certo.

Conversão de tipos numéricos

Isso muitas vezes acontece automaticamente:

julia> x = 2 + 3.5
5.5

julia> typeof(x)
Float64

Somamos um Int64 a um Float64 e obtivemos um resultado Float64.

Na verdade, o inteiro foi convertido silenciosamente para Float64 antes de a soma ser feita.

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

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

Se uma mistura de tipos for usada em uma expressão, cada um é "promovido" conforme necessário para o tipo mais largo em uso.

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

Como alternativa, basta usar float(5) e deixar o compilador escolher um tipo adequado.

As conversões de ponto flutuante para inteiro são inevitavelmente mais complicadas. O que você quer fazer com o que vem depois do ponto decimal?

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

No entanto, por padrão, essas funções não retornam o tipo inteiro que você talvez quisesse. É possível especificar o tipo de saída desejado.

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 específico de dígitos depois do ponto decimal com a palavra-chave digits.

julia> round(π, digits=10)
3.1415926536

Consulte o manual para mais detalhes.

Divisão por zero

Com certeza isso só gera um erro, não é? Na verdade, a situação não é tão simples assim.

A divisão inteira com ÷ ou // resulta em um erro, como você provavelmente espera.

A divisão de ponto flutuante com / adota o que poderia ser considerado uma abordagem de engenharia, em vez da abordagem padrão da ciência da computação:

julia> 2 / 0
Inf

julia> 0 / 0
NaN

Como discutido em uma seção anterior, o infinito é um número de ponto flutuante válido em Julia, representado por Inf.

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

Se isso parecer estranho, pense no contexto de trabalhar com arrays grandes de dados do mundo real (e, portanto, muitas vezes bastante bagunçados). Para seguir em frente, o melhor é apenas sinalizar os valores problemáticos e continuar.

Verificar manualmente os valores sem parar seria tedioso de programar e certamente prejudicaria o desempenho em tempo de execução.

Parar com uma mensagem de erro a cada pequeno problema deixaria o seu programa muito impopular entre os usuários!

Comparar valores de ponto flutuante

Como descrito no conceito Conditionals, o teste de igualdade geralmente é feito com o operador ==.

Isso funciona bem para inteiros, caracteres, strings etc. No entanto, os valores de ponto flutuante têm precisão limitada, e maneiras diferentes de calcular o mesmo resultado podem levar a valores ligeiramente diferentes. Para Float64, isso normalmente ocorre por volta do 15º algarismo significativo: uma diferença pequena, mas que não é "igualdade".

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

Uma alternativa amplamente usada é testar o valor absoluto da diferença contra alguma tolerância permitida (muitas vezes chamada de epsilon ou ϵ). Então, em vez de a == b, use abs(a - b) < epsilon.

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

Para uma tolerância absoluta, a sintaxe é isapprox(a, b, atol=epsilon). Nesse caso, a palavra-chave atol é obrigatória.

A tolerância relativa costuma ser mais útil. Ela é o padrão, então isapprox(a, b) vai tentar escolher um valor razoável para rtol (as regras são bastante complicadas).

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

Ao estilo habitual de Julia, existe um operador matemático sinônimo do caso padrão: a ≈ b (com o operador digitado como \approx e depois <tab>).

Conceitos futuros relacionados

Além dos números racionais, partes posteriores do currículo vão discutir:

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Números

A prática está bloqueada

Desbloqueie mais 2 exercícios para praticar Números