Julia es un lenguaje de propósito general que se puede usar para la mayoría de las tareas de programación. En la práctica, sin embargo, sus principales casos de uso tienden a estar en la ingeniería y la ciencia. Los cálculos numéricos rápidos, versátiles y sofisticados son el eje de su diseño.
Un entero es un número «redondo», sin coma decimal.
En el concepto Basics vimos que se puede asignar un valor entero a una variable sin especificar un tipo.
Para mejorar la legibilidad se pueden usar guiones bajos como separador de dígitos. El compilador los ignora.
julia> x = 3
3
julia> typeof(x)
Int64
julia> large_number = 1_234_567_890
1234567890
Internamente, el compilador usará el tipo de entero con signo que resulte más apropiado para tu CPU.
En los ordenadores modernos, normalmente será Int64, que es perfectamente adecuado para la mayoría de las tareas.
Los tipos se tratarán con más detalle en un concepto futuro. Por ahora, los tipos numéricos deberían ser lo bastante intuitivos como para que este documento tenga sentido.
Como veremos en un concepto posterior, Julia admite de forma nativa arrays muy grandes y multidimensionales. Estos pueden poner a prueba tanto la cantidad de memoria disponible como el ancho de banda para las transferencias de datos.
Para dar al programador un control más preciso, es posible y bastante habitual especificar el tipo de entero con signo o sin signo deseado. Muchas personas que aprenden Julia y que antes han usado Python quizá quieran comparar esto con NumPy, en lugar de con el Python básico.
julia> y = Int8(42)
42
julia> typeof(y)
Int8
julia> z = UInt32(1024)
0x00000400
Fíjate en que Julia, de forma predeterminada, muestra los enteros sin signo en formato hexadecimal, porque los tipos sin signo se usan a menudo para operaciones de bits de bajo nivel.
Los enteros también se pueden escribir en binario, octal o hexadecimal, con los prefijos 0b, 0o y 0x, respectivamente.
julia> a = 0x10
0x10
julia> Int(a)
16
julia> b = 0b1010 # displays as hexadecimal
0x0a
BigInt
Cada tipo de entero tiene un valor máximo y un valor mínimo que puede almacenar:
julia> typemax(Int8)
127
julia> typemin(Int8)
-128
Salirse de este rango válido provoca un desbordamiento de enteros, con resultados que parecen muy extraños.
julia> c = Int8(126)
126
julia> c * c
4
Seguramente estaremos de acuerdo en que 126 * 126 no debería ser igual a 4.
Una explicación detallada requiere saber cómo los registros de la CPU almacenan los enteros, algo que queda fuera del alcance de este concepto.
El problema se puede minimizar usando un tipo «más ancho», hasta Int128 o UInt128 (el significado de «más ancho» se explica más abajo). Sin embargo, todos tienen límites estrictos antes de toparse con problemas de desbordamiento.
Una solución es usar el tipo BigInt, que solo está limitado por la memoria de tu ordenador.
Es la implementación que hace Julia de la aritmética de precisión arbitraria.
julia> 2 ^ 70
0
julia> big = BigInt(2) ^ 70
1180591620717411303424
julia> UInt128(big)
0x00000000000000400000000000000000
Como 2 elevado a 70 no cabe en un Int64, el primer cálculo del ejemplo anterior falla, aunque fíjate en que no hay ningún mensaje de error.
¡Cuidado, programador!
La conversión de tipo de 2 a un BigInt da la respuesta correcta, y mostrarlo en hexadecimal deja más claro que es una potencia de 2.
No sorprenderá que los números en coma flotante tengan, de forma opcional, una coma decimal y una parte fraccionaria después de la coma.
julia> f = 3.45
3.45
julia> typeof(f)
Float64
Por supuesto, se admite la notación científica.
julia> avogadro = 6.02e23
6.02e23
Al igual que con los enteros, el tipo predeterminado es suficiente para la mayoría de los propósitos, pero hay otros tipos con signo disponibles. No existen tipos de coma flotante sin signo.
Como atajo, se pueden crear valores Float32 usando el sufijo f0.
julia> f32 = 4.56f0
4.56f0
julia> typeof(f32)
Float32
Otros tamaños de números en coma flotante necesitan una conversión de tipo explícita, igual que ocurre con los enteros.
Los valores máximo y mínimo pueden sorprenderte:
julia> typemax(Float64)
Inf
julia> typemin(Float64)
-Inf
¡El infinito es un valor válido!
Sin embargo, el rango útil de los números en coma flotante es limitado: cualquier valor muy grande recibe simplemente el valor Inf, y cualquier valor muy pequeño se redondea a 0.0.
Podemos usar otro par de funciones para ver estos límites: aproximadamente ± 10 ^ 308.
julia> floatmax(Float64)
1.7976931348623157e308
julia> floatmin(Float64)
2.2250738585072014e-308
Además, la precisión es limitada, ya que se representa un número grande pero finito de dígitos significativos (aproximadamente 15 para Float64).
Como se explicó en el concepto Basics, los operadores aritméticos funcionan casi igual que la aritmética estándar que se enseña a los niños.
Fíjate en que la exponenciación usa ^, no ** (ambos son habituales en otros lenguajes).
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)
Sin embargo, merece la pena comentar algunos detalles específicos de Julia.
julia> x = 4.2
4.2
julia> 2 * x
8.4
julia> 2x
8.4
julia> 2.4x
10.08
Eso puede sorprender.
Siempre es posible usar * como operador infijo, como en la mayoría de los demás lenguajes informáticos.
Sin embargo, Julia está diseñado por personas que creen que el código debería parecerse lo máximo posible a las ecuaciones matemáticas.
Como los nombres de las variables deben empezar por una letra, anteponer un número (entero o en coma flotante) al nombre se interpreta como una multiplicación implícita.
Por ejemplo, si queremos el área de la superficie de una esfera, en lugar de 4 * pi * r * r podríamos hacer esto:
julia> surface(r) = 4π * r^2
surface (generic function with 1 method)
julia> surface(3)
113.09733552923255
Aunque π es una constante incorporada, también es una letra (griega).
Por eso, el analizador sintáctico sigue necesitando un * explícito para separar π de r.
Usar / como operador infijo siempre da un resultado en coma flotante, incluso cuando las entradas son números enteros.
Para la división entera hay más opciones:
julia> 10 / 3
3.3333333333333335
julia> div(10, 3)
3
julia> 10 ÷ 3
3
julia> 10 // 3
10//3
La función div() sirve para la división entera, y el resultado se trunca hacia cero: hacia abajo para los números positivos y hacia arriba para los negativos.
Como sinónimo, podemos usar el operador infijo ÷, de nuevo con la intención de que parezca más matemático.
Si usas un editor que entiende Julia, escribe \div y luego pulsa la tecla <Tab>.
El operador // necesitará un concepto propio más adelante en el temario.
Por ahora, basta con decir que el resultado de // es un número «racional», el nombre formal de lo que la mayoría de la gente llama fracción.
Se eliminarán los factores comunes del numerador y el denominador, para dar una razón de dos enteros en lo que se llama «mínima expresión».
julia> rationalnum = 22 // 6
11//3
julia> typeof(rationalnum)
Rational{Int64}
Ya tenemos los números racionales. ¿Y los números «irracionales»?
julia> π
π = 3.1415926535897...
julia> typeof(π)
Irrational{:π}
Un número irracional es aquel que no se puede reducir a una razón de enteros.
Ejemplos habituales son π, e (el número de Euler) y muchas raíces, como √2 (la raíz cuadrada de 2).
Julia intenta hacer matemáticas como es debido.
Esto a menudo ocurre de forma automática:
julia> x = 2 + 3.5
5.5
julia> typeof(x)
Float64
Sumamos un Int64 a un Float64 y obtuvimos un resultado Float64.
De hecho, el entero se convirtió silenciosamente a Float64 antes de hacer la suma.
Julia tiene el concepto de «anchura» de los tipos numéricos.
Int64 es más ancho que Int16.Si se usa una mezcla de tipos dentro de una expresión, cada uno se «promociona» según sea necesario al tipo más ancho que se use.
Para forzar la conversión, podemos convertir un entero a un tipo específico, como en Float64(5).
Como alternativa, basta con usar float(5) y dejar que el compilador elija un tipo apropiado.
Las conversiones de coma flotante a entero son inevitablemente más complicadas. ¿Qué quieres hacer con lo que haya después de la coma decimal?
round() convierte al número entero más cercano, y en los empates, como 4,5, redondea al número entero par más cercano.floor() redondea hacia abajo, ceil() hacia arriba y trunc() hacia cero.Int32(), fallará con un InexactError.Sin embargo, de forma predeterminada, estas funciones no devuelven el tipo de entero que quizá querías. Se puede especificar el tipo de salida deseado.
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
También es posible redondear a un número concreto de dígitos después de la coma decimal con la palabra clave digits.
julia> round(π, digits=10)
3.1415926536
Consulta el manual para más detalles.
¿Seguro que esto simplemente lanza un error? En realidad, la situación no es tan sencilla.
La división entera con ÷ o // dará lugar a un error, como cabría esperar.
La división en coma flotante con / adopta lo que podría considerarse un enfoque de ingeniería, más que un enfoque estándar de la informática:
julia> 2 / 0
Inf
julia> 0 / 0
NaN
Como se explicó en una sección anterior, el infinito es un número en coma flotante válido en Julia, representado por Inf.
Cuando el numerador también es cero, el resultado está matemáticamente indefinido.
Entonces Julia lo trata como «no es un número», representado por NaN.
Si esto te parece extraño, piénsalo en el contexto de trabajar con arrays grandes de datos del mundo real (y, por tanto, a menudo bastante desordenados). Para avanzar, lo mejor es simplemente marcar los valores problemáticos y seguir adelante.
Comprobar los valores manualmente sin fin sería tedioso de programar y, sin duda, perjudicaría el rendimiento en tiempo de ejecución.
¡Detenerse con un mensaje de error ante cada mínima anomalía haría que tu programa resultara muy impopular entre los usuarios!
Como se describe en el concepto Conditionals, las comprobaciones de igualdad se suelen hacer con el operador ==.
Esto funciona bien con enteros, caracteres, strings, etc.
Sin embargo, los valores en coma flotante tienen una precisión limitada, y distintas formas de calcular el mismo resultado pueden dar valores ligeramente diferentes.
Para Float64, esto suele estar en torno al decimoquinto dígito significativo: una diferencia pequeña, pero no «igualdad».
Tradicionalmente, el consejo para los programadores es no usar nunca == con valores en coma flotante: los resultados son impredecibles.
Una alternativa muy usada es comprobar el valor absoluto de la diferencia frente a cierta tolerancia permitida (a la que a menudo se llama épsilon o ϵ).
Así, en lugar de a == b, usa abs(a - b) < epsilon.
Julia ofrece una alternativa más limpia con la función isapprox().
Para una tolerancia absoluta, la sintaxis es isapprox(a, b, atol=epsilon).
En este caso, la palabra clave atol es obligatoria.
La tolerancia relativa suele ser más útil.
Esta es la predeterminada, así que isapprox(a, b) intentará elegir un valor razonable para rtol (las reglas son bastante complicadas).
También se puede especificar una tolerancia relativa, como una fracción de los valores que se comparan.
Así, isapprox(a, b, rtol=0.01 comprueba que los valores están dentro del 1 % el uno del otro.
Al estilo habitual de Julia, hay un operador matemático que es sinónimo del caso predeterminado: a ≈ b (el operador se escribe como \approx y luego <tab>).
Además de los números racionales, otras partes posteriores del temario tratarán:
2.3 + 4.5im.