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 suelen estar en la ingeniería y la ciencia. Los cálculos numéricos rápidos, versátiles y sofisticados son centrales en su diseño.
Un número entero es un número "redondo" sin punto decimal.
En el concepto Básicos vimos que se puede asignar un valor entero a una variable sin especificar un tipo.
Por 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 sea más apropiado para tu CPU.
En las computadoras modernas esto suele 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, esperamos que los tipos numéricos sean lo bastante intuitivos para que este documento tenga sentido.
Como veremos en un concepto posterior, Julia admite de forma nativa arrays multidimensionales muy grandes. Estos pueden poner a prueba tanto la cantidad de memoria disponible como el ancho de banda para las transferencias de datos.
Para dar un control más fino al programador, es posible y bastante común especificar el tipo de entero con signo o sin signo que se desee. Los muchos estudiantes de Julia que antes usaron Python quizá quieran compararlo con NumPy, en lugar del Python básico.
julia> y = Int8(42)
42
julia> typeof(y)
Int8
julia> z = UInt32(1024)
0x00000400
Ten en cuenta que, por defecto, Julia muestra los enteros sin signo en formato hexadecimal, porque los tipos sin signo se suelen usar 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 mínimo que puede almacenar:
julia> typemax(Int8)
127
julia> typemin(Int8)
-128
Salir de este rango válido provocará un desbordamiento de enteros, con resultados que parecen muy extraños.
julia> c = Int8(126)
126
julia> c * c
4
Seguramente estemos de acuerdo en que 126 * 126 no debería ser igual a 4.
Una explicación detallada requiere conocer 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 computadora.
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 la 70 no cabe en un Int64, el primer cálculo del ejemplo anterior falla, aunque hay que notar que no hay ningún mensaje de error.
¡Cuidado, programador!
Convertir 2 a un BigInt con un cast da la respuesta correcta, y mostrarlo en hexadecimal deja más claro que es una potencia de 2.
No sorprenderá que los números de punto flotante tengan, de forma opcional, un punto decimal y una parte fraccionaria después del punto.
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
Como con los enteros, el tipo predeterminado sirve para la mayoría de los propósitos, pero hay otros tipos con signo disponibles. No existen tipos de punto 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 float necesitan un cast explícito, igual que 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 de punto flotante es limitado: cualquier valor muy grande simplemente recibe un 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 Básicos, los operadores aritméticos funcionan casi igual que la aritmética estándar que se enseña a los niños.
Ten en cuenta que la exponenciación usa ^, no ** (ambos son comunes 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, vale 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 otros lenguajes de programación.
Sin embargo, Julia fue diseñado por personas que creen que el código debería parecerse lo más posible a las ecuaciones matemáticas.
Como los nombres de las variables deben empezar con una letra, anteponer un número (entero o de punto flotante) al nombre se trata 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 todavía necesita un * explícito para separar π de r.
Usar / como operador infijo siempre da un resultado de punto flotante, incluso con entradas enteras.
Para la división de enteros 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() es para la división de enteros, con el resultado truncado hacia cero: hacia abajo para los números positivos, hacia arriba para los negativos.
Como sinónimo, podemos usar el operador infijo ÷, de nuevo con la intención de que se vea más matemático.
Si usas un editor que entiende Julia, escríbelo como \div y luego presiona 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 eliminan 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}
Tenemos 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 comunes son π, e (el número de Euler) y muchas raíces como √2 (la raíz cuadrada de 2).
Julia intenta hacer matemáticas correctamente.
Esto a menudo puede ocurrir 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 un concepto del "ancho" 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 "promueve" según sea necesario al tipo más ancho utilizado.
Para forzar la conversión, podemos convertir un entero a un tipo específico, como en Float64(5).
Como alternativa, simplemente usa float(5) y deja que el compilador elija un tipo apropiado.
Las conversiones de punto flotante a entero son inevitablemente más complicadas. ¿Qué quieres hacer con lo que está después del punto 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(), obtendrás un error InexactError.Sin embargo, por defecto 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 especificado de dígitos después del punto decimal con la palabra clave digits.
julia> round(π, digits=10)
3.1415926536
Consulta el manual para más detalles.
¿Acaso esto no lanza simplemente un error? En realidad, la situación no es tan sencilla.
La división de enteros con ÷ o // da un error, como quizá esperes.
La división de punto flotante con / adopta lo que podría considerarse un enfoque de ingeniería, en lugar del enfoque estándar de las ciencias de la computación:
julia> 2 / 0
Inf
julia> 0 / 0
NaN
Como se explicó en una sección anterior, el infinito es un número de punto flotante válido en Julia, representado por Inf.
Cuando el numerador también es cero, el resultado es 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 grandes arrays de datos del mundo real (y por lo tanto, a menudo bastante desordenados). Para seguir avanzando, lo mejor es simplemente marcar los valores problemáticos y continuar.
Programar una revisión manual e interminable de los valores sería tedioso y, sin duda, perjudicaría el rendimiento en tiempo de ejecución.
¡Detenerse con un mensaje de error ante el más mínimo fallo haría que tu programa fuera muy impopular entre los usuarios!
Como se describe en el concepto Conditionals, la comprobación de igualdad suele hacerse con el operador ==.
Esto funciona bien para enteros, caracteres, strings, etc.
Sin embargo, los valores de punto flotante tienen una precisión limitada, y distintas formas de calcular el mismo resultado pueden dar valores ligeramente diferentes.
Para Float64, esto suele estar alrededor del decimoquinto dígito significativo: una diferencia pequeña, pero no "igualdad".
Tradicionalmente, el consejo para los programadores es nunca usar == con valores de punto flotante: los resultados son impredecibles.
Una alternativa muy usada es comprobar el valor absoluto de la diferencia contra 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 opción predeterminada, así que isapprox(a, b) intentará elegir algún 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% entre sí.
Al estilo habitual de Julia, hay un operador matemático que es sinónimo del caso predeterminado: a ≈ b (con el operador escrito como \approx y luego <tab>).
Además de los números racionales, en partes posteriores del temario se tratarán:
2.3 + 4.5im.