Ti

Tipos en Julia

1 ejercicio

Acerca de Tipos

Hasta ahora, el temario no ha dicho mucho sobre los tipos, pero claramente existen en Julia:

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.

  1. Julia tiene types, que son centrales en su diseño.
  2. Julia normalmente puede «adivinar» el tipo, usando la Type Inference.

El compilador JIT revisará (todo) el código, verá cómo se usa una variable e inferirá un tipo predeterminado adecuado que sea compatible con ese uso.

Type promotion

Ha habido muchos ejemplos en conceptos anteriores que obligan a los tipos numéricos a una uniformidad, empezando por la aritmética más simple.

julia> 2 + 1.3
3.3

Sumamos un número entero (Int64) y uno de punto flotante (Float64) y obtuvimos un resultado Float64. De manera 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 convertir a un vector todos cambiaron a un Float64 uniforme.

El compilador de Julia entiende qué conversiones de tipo son posibles: de entero a flotante no hay problema; de flotante a entero se pierde precisión, así 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 todos los valores de entrada.

La misma conversión se puede hacer de forma explícita con la función promote():

julia> promote(nums...)
(3.0, 4.1, 0.25)

Type assignment

Confiar en la inferencia y la promoción de tipos está bien para resolver ejercicios sencillos de tutorial, pero para programas más grandes es probable que necesites un control más preciso.

En la mayoría de los procesadores modernos, un entero es Int64 de forma predeterminada. Vimos en el concepto Numbers que un valor se puede convertir a un tipo particular distinto del predeterminado.

julia> x = Int16(42)
42

julia> typeof(x)
Int16

Sin embargo, la variable x todavía se puede reasignar a un tipo diferente:

julia> x = "changed"
"changed"

julia> typeof(x)
String

Esto es type instability, que es:

  • Muy conveniente en scripts pequeños.
  • Malo para el rendimiento y la confiabilidad en programas más grandes.

En cambio, podemos establecer 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, y siempre será, de tipo Int16. Así, el compilador sabe cuántos bytes reservar para ella y puede optimizar el resto del código para confiar en un tipo estable. En esto, la variable es más o menos similar a las de un lenguaje de tipado estático como C.

Type Assertion

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 a un valor, es en general una aserción de que el valor debe ser de este tipo; de lo contrario, se debería lanzar un error.

julia> 42::Number
42

julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String

Con frecuencia, esto se puede usar con el valor de retorno de una función, como una última verificació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.

La jerarquía de tipos

Int64, Int16, String, Char: ¿de dónde «vienen» estos tipos?

En muchos lenguajes orientados a objetos (POO), cada tipo es una clase, las subclases las 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 afectan 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 científico de la computación durante varias décadas desde los años cincuenta (aunque no es conocido precisamente por su optimismo soleado ni por su diplomacia sutil).

Y aun así, 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)

Para completar algunos detalles:

  • 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.
  • Hay una jerarquía de tipos que sube por Integer, Real y Number hasta Any en la cima, y subtypes() nos enumerará esta rama de la jerarquía.

Todas las ramas terminan en Any, que es único por ser su propio supertipo.

julia> supertypes(String)
(String, AbstractString, Any)

julia> supertype(Any)
Any

Entonces, Julia no tiene una jerarquía de class, pero sí tiene una jerarquía de type. Intentar mostrar la jerarquía completa da un árbol enorme, imposible de ver realmente. Mirar partes de ella es algo que se ha discutido en línea.

Con el tiempo, intentaremos desentrañar cómo funciona esto, pero antes hay mucho más por explorar.

Comprobación de tipos

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 la jerarquía 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. Se puede usar 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 manera 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 se puede usar para controlar el flujo dentro de una función, pero esto es relativamente inusual en Julia idiomático.

Veremos en el concepto Multiple Dispatch que a menudo es más eficiente agregar 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.

Tipos abstractos y tipos concretos

Vimos que la jerarquía de tipos forma una estructura de árbol (en el sentido de las ciencias de la computación, con la raíz en la cima).

Cada elemento del árbol es un node, y estos se pueden dividir en categorías:

  1. Los nodos con subtipos se llaman abstract.
  2. Los nodos hoja, sin subtipos, se llaman concrete.
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 se 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 un mensaje de error (en este caso), pero el compilador crea un tipo concreto apropiado: Int64 en lugar de Integer. Al menos evita que se asigne a la variable un valor que no sea 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 cualquier subtipo de (en el caso anterior) Integer. Esto es algo inusual 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() y isconcretetype() permiten comprobarlo. Ten en cuenta que no son simplemente negaciones una de la otra: veremos en un concepto posterior que algunos tipos no pueden ser 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, los elementos sí lo son. La función eltype() (tipo de elemento) extrae este tipo:

julia> eltype([1, 2.3])
Float64
Editar en GitHub El enlace se abre en una ventana o pestaña nueva

Aprende Tipos