Hasta ahora, el temario no ha dicho mucho sobre los tipos, pero está claro que en Julia existen:
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
Nunca especificamos los tipos, pero Julia los asignó de todos modos.
types, que ocupan un lugar central en su diseño.Type Inference.El compilador JIT examinará (todo) el código, verá cómo se usa una variable e inferirá un tipo predeterminado adecuado y compatible con ese uso.
En conceptos anteriores ha habido muchos ejemplos que fuerzan los tipos numéricos a una forma uniforme, empezando por la aritmética más simple.
julia> 2 + 1.3
3.3
Sumamos un número entero (Int64) y un número de coma flotante (Float64) y obtuvimos un resultado Float64. De forma similar:
julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)
julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}
julia> [nums...]
3-element Vector{Float64}:
3.0
4.1
0.25
En el caso anterior, una tupla conservó el tipo de cada elemento, pero al convertirla en un vector todos cambiaron a un Float64 uniforme.
El compilador de Julia sabe qué conversiones de tipos son posibles: de entero a coma flotante no hay problema; de coma flotante a entero se pierde precisión, por lo que se lanza un InexactError.
La promoción de tipos convierte todos los valores de la expresión a un tipo común, lo bastante versátil como para ser compatible con todas las entradas.
La misma conversión puede hacerse de forma explícita con la función promote():
julia> promote(nums...)
(3.0, 4.1, 0.25)
Confiar en la inferencia de tipos y la promoción de tipos está bien para resolver ejercicios sencillos de tutorial, pero en programas más grandes es probable que necesites un control más preciso.
En la mayoría de los procesadores modernos, un número entero es de tipo Int64 de forma predeterminada.
Vimos en el concepto Numbers que un valor puede convertirse a un tipo concreto distinto del predeterminado.
julia> x = Int16(42)
42
julia> typeof(x)
Int16
Sin embargo, la variable x todavía puede reasignarse a un tipo diferente:
julia> x = "changed"
"changed"
julia> typeof(x)
String
Esto es type instability, que es:
En su lugar, podemos fijar el tipo de x usando el operador :::
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.
Ahora y es de tipo Int16, y siempre lo será.
Así, el compilador sabe cuántos bytes debe reservar y puede optimizar el resto del código apoyándose en un tipo estable.
En esto, la variable se parece más o menos a las de un lenguaje de tipado estático como C.
La asignación de tipos, descrita antes, se usa en el lado izquierdo de una asignación de variable para restringir el tipo de esa variable.
Usar el operador :: con un valor, o con algo que se evalúa como un valor, es en general una aserción de que el valor debe ser de ese tipo; de lo contrario, debería lanzarse un error.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
Es habitual usarlo con el valor devuelto por una función, como una última comprobación sencilla de que la función se comportó como se esperaba.
Ten en cuenta que existe una macro @assert para otras formas de aserción.
Int64, Int16, String, Char: ¿de dónde "vienen" estos tipos?
En muchos lenguajes orientados a objetos (POO), cada tipo es una clase, las subclases los organizan en una jerarquía de clases y los métodos de clase definen los comportamientos.
Java y Ruby son ejemplos claros de este patrón, pero incluso Python es similar internamente.
Julia no tiene clases.
La razón documentada es que las características de la POO interfieren con el compilador JIT y perjudican el rendimiento en tiempo de ejecución.
Quién sabe, quizá también leyeron esta cita:
«La programación orientada a objetos es una idea excepcionalmente mala que solo podría haber surgido en California.»
Se atribuye a Edsger Dijkstra, un brillante informático durante varias décadas desde los años cincuenta (aunque no se le conoce precisamente por su optimismo soleado ni por su diplomacia sutil).
Y, sin embargo, mira este código:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
Con algunos detalles más:
Int16 es un tipo, y podemos crear variables de este tipo.Int16 es un subtipo de Signed, y la función supertype() nos lo muestra.Integer, Real y Number hasta Any en la cima, y subtypes() nos mostrará esta rama de la jerarquía.Todas las ramas terminan en Any, que tiene la particularidad de ser su propio supertipo.
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
Así que Julia no tiene una jerarquía de class, pero sí tiene una jerarquía de type.
Intentar mostrar la jerarquía completa da como resultado un árbol enorme, imposible de ver de verdad.
Examinar partes de ella es algo que se ha debatido en línea.
Con el tiempo, intentaremos desentrañar cómo funciona esto, pero antes hay mucho más que explorar.
Podemos usar typeof() para comprobar la igualdad de la forma habitual.
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
La igualdad de tipos debe ser exacta, ya que esta forma de comparación no entiende de jerarquías de tipos.
De forma más flexible, isa nos dirá si un valor tiene el mismo tipo que un comparador o un subtipo de este.
Puede usarse tanto en forma infija como en forma de función.
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
Ten en cuenta que isa espera un value a la izquierda, no un type.
Intentar comparar dos tipos de esta forma dará resultados inesperados.
El operador correcto es <:, que veremos mucho más en conceptos futuros.
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
Comprobar tipos puede usarse para controlar el flujo dentro de una función, pero esto es relativamente poco habitual en Julia idiomático.
Veremos en el concepto Multiple Dispatch que a menudo es más eficiente añadir tipos a los argumentos de una función y dejar que el mecanismo de despacho de Julia se encargue de esa lógica. Sin embargo, hay varios conceptos más relacionados con los tipos que debemos tratar antes de llegar a Multiple Dispatch.
Vimos que la jerarquía de tipos forma una estructura de árbol (en el sentido de la informática, con la raíz arriba).
Cada elemento del árbol es un node, y estos pueden dividirse en categorías:
julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
Esta es una distinción importante, porque solo los tipos concretos pueden instantiated como variables.
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
Ten en cuenta que intentar usar un tipo abstracto no da ningún mensaje de error (en este caso), pero el compilador crea un tipo concreto adecuado: Int64 en lugar de Integer.
Al menos impide que la variable se asigne a algo que no sea un número entero:
julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.
La asignación de tipos con un tipo abstracto es, por tanto, una restricción sobre el tipo de la variable, a cualquiera de los subtipos de (en el caso anterior) Integer.
Esto es algo poco habitual en el mundo de los lenguajes de programación: más débil que una asignación de tipos en C, más fuerte que una sugerencia de tipo en versiones recientes de Python.
Las funciones isabstracttype() e isconcretetype() permiten comprobarlo.
Ten en cuenta que no son simplemente la negación una de la otra: veremos en un concepto posterior que algunos tipos no son ni abstractos ni concretos.
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
Aunque Vector no es un tipo concreto, sus elementos sí lo son.
La función eltype() (tipo de elemento) extrae ese tipo:
julia> eltype([1, 2.3])
Float64