14 Tipos de Variables en Julia

Note: En este notebook vamos a discutir una breve introducción a la lógica del despacho múltiple y de como manejar (declarar) tipos de variables en Julia. Para un tratamiento profundo ver la sección tipos en el manual.

14.1 Despacho Múltiple

Julia es un lenguaje de programación en el cuál los tipos de variables son claves para hacer los procesos eficientes y de hecho la la filosofía del despacho múltiple descansa sobre esta idea. Despacho múltiple significa que las funciones despacharán distintos procesos (llamados métodos en Julia) según el tipo de variables que hayan sido provistos como inputs. La clave entonces es que una vez que se compila un procedimiento, el método particular asociado al tipo de variables será usado siempre con esa variable (note que la clave entonces es que el tipo no cambio, esto es exista estabilidad de tipos). Esto es lo que hace que Julia sea bastante eficiente. Por supuesto, si un programa tiene inestabilidad de tipos, los métodos irán cambiando y será necesario compilar nuevamente los procesos (hecho que ocurre instantáneamente por el JIT Just-in-Time-Compilation). Esto puede hacer que Julia sea tan lento e ineficiente como otros lenguajes de alto nivel. Por esta razón Julia permite declarar explícitamente y de forma simple los tipos de variables.

Si el programador no provee los tipos de variables explícitamente, Julia “adivina” el tipo y aplica el método correspondiente. Tomemos como ejemplo la siguiente función sin definir explícitamente el tipo de su input:

function multiplica(x,y)
    return x*y
end
multiplica (generic function with 1 method)

Si aplicamos la función a una variable Float64, Julia aplica el método correspondiente a la multiplicación de variables Float64:

multiplica(2.0,3.0)
6.0

Si aplicamos la función a una variable String, Julia aplica el método correspondiente que en este caso es concatenar variables String:

multiplica("hola ","Pedro")
"hola Pedro"

Esto ocurre porque Julia sabe que hacer dependiendo del tipo de \(x\). De hecho, *() es un función y tiene 346 métodos (para saber el métodos de una función usamos methods(función)). Ahora, si pasamos un vector por la función cuadrado obtenemos:

multiplica([2, 3, 4, 5],[7, 8, 1, 2])
MethodError: no method matching *(::Array{Int64,1}, ::Array{Int64,1})
Closest candidates are:
  ...



Stacktrace:

 [1] multiplica(::Array{Int64,1}, ::Array{Int64,1}) at ./In[1]:2

 [2] top-level scope at In[4]:1

Este error ocurre porque *() no tiene definido ningún método para multiplicar vectores (si tiene un método para trabajar con matrices que cumplen las reglas de conformabilidad de la multiplicación).

Para entender el concepto de despacho múltiple, creamos la función mifuntipo() declarando explícitamente el tipo del input (esto se logra con variables::tipo inmediatamente después de la variable). Si el input es un Int64, la función imprimirá en pantalla que tenemos un Int64:

function mifuntipo(x::Int64)
    println("$x es del tipo Int64")
end
mifuntipo (generic function with 1 method)

Ahora creamos la misma función pero en este caso le pedimos que si el input es un Float64 la función imprima en pantalla que tenemos un Float64:

function mifuntipo(x::Float64)
    println("$x es del tipo Float64")
end
mifuntipo (generic function with 2 methods)

Note que Julia nos indica que se creó mifuntipo() nuevamente pero ahora con 2 métodos. Repitamos una vez más la definición de la función pero ahora con un input del tipo String. En este caso, la función imprimirá en pantalla que tenemos un String:

function mifuntipo(x::String)
    println("$x es del tipo String")
end
mifuntipo (generic function with 3 methods)

Esta función tiene tres métodos y sabe que hacer cuando se le provee un Int64, un Float64 o un String:

methods(mifuntipo)
3 methods for generic function mifuntipo:
  • mifuntipo(x::String) in Main at In[7]:2
  • mifuntipo(x::Float64) in Main at In[6]:2
  • mifuntipo(x::Int64) in Main at In[5]:2
mifuntipo(2)
2 es del tipo Int64
mifuntipo(2.0)
2.0 es del tipo Float64
mifuntipo("palabra")
palabra es del tipo String

Si pasamos como argumento una variable tipo Booleana (Bool) obtendremos un error porque la función mifuntipo() no tiene ningún método para este tipo de variable:

mifuntipo(true)
MethodError: no method matching mifuntipo(::Bool)
Closest candidates are:
  mifuntipo(!Matched::String) at In[7]:2
  mifuntipo(!Matched::Float64) at In[6]:2
  mifuntipo(!Matched::Int64) at In[5]:2



Stacktrace:

 [1] top-level scope at In[12]:1

Para forzar el tipo de del resultado de una función usamos function nombre_funcion(args)::tipo. Por ejemplo, generamos una función que calcule el logaritmo de un número y que el resultado siempre sea un Float64:

function logc(x)::Float64
    if x == 0
        return 1
    end
    return log(x)
end
logc (generic function with 1 method)
typeof(logc(0))
Float64

14.2 Tipos Compuestos

En algunos contextos es útil crear una estructura de datos que contenga diversas variables, funciones, y otros objetos y que el tipo de todos los anteriores este predeterminado. Estas estructuras pueden posteriormente ser pasadas por una función para realizar procedimientos usando los datos contenidos en la estructura. La sintaxis es:

struct Nombre_Estructura
    contenido...
end

Por convención la usamos mayúsculas para la primera letra al nombrar una estructura.

Nota: La estructura creada generará casos inmutables, esto es no se podrá cambiar la información contenida una vez creados casos particulares usando la estructura. En algunos contexto puede ser útil crear una estructura que sea mutable, esto es que se pueda por ejemplo modificar el valor de una variable de forma posterior a su creación. Para ello usamos la siguiente sintaxis:

mutable struct Nombre_Estructura
    contenido...
end

Como ejemplo vamos a crear la siguiente estructura:

struct Trabajador
   nombre::String
   edad::Int
   salario::Float64 
end

Ahora que tenemos la estructura podemos crear diferentes contenedores de información usando la estructura de contenido (cada objeto creado representa un caso distinto y contiene diferente información):

datos_trabajador1 = Trabajador("Joe Doe", 23, 600000)
datos_trabajador2 = Trabajador("Jane Doe", 22, 800000);

Obtenemos la información usando el formato: nombre_caso.objeto. Por ejemplo:

datos_trabajador1.edad
23
datos_trabajador2.salario
800000.0

Note que no es posible modificar la información de un caso particular una vez creado (el caso es inmutable):

datos_trabajador1.edad = 25
setfield! immutable struct of type Trabajador cannot be changed



Stacktrace:

 [1] setproperty!(::Trabajador, ::Symbol, ::Int64) at ./sysimg.jl:19

 [2] top-level scope at In[19]:1

Si creamos una estructura que genera casos mutable es posible lo anterior. Por ejemplo:

mutable struct Trabajador2
   nombre::String
   edad::Int
   salario::Float64 
end

Creamos un caso:

nuevos_datos_trabajador1 = Trabajador2("Joe Doe", 23, 600000)
Trabajador2("Joe Doe", 23, 600000.0)

Cambiamos la edad del trabajador:

nuevos_datos_trabajador1.edad = 34
34

Tenemos el mismo caso, pero ahora con una variables modificada:

nuevos_datos_trabajador1
Trabajador2("Joe Doe", 34, 600000.0)

El paquete Parameters extiende la funcionalidad de estas estruturas para utilizarlas como contenedores de parámetros. Más aún, dichos contenedores pueden o no tener vealores por defecto y pueden ser fácilmente desempaquetados. Para iniciar una estructura con valores por defecto usamos la macro @with_kw. Por ejemplo, creamos una estructura con dos parámetros \(\alpha=0.33\) y \(\beta=0.99\) y una función \(\log(x)\).

using Parameters
@with_kw struct Modelo
    α::Float64 = 0.33
    β::Float64 = 0.99
    u::Function = x -> log(x)
end
Modelo

Inicializamos un primer contenedor con valores por defecto:

modelo1 = Modelo()
Modelo
  α: Float64 0.33
  β: Float64 0.99
  u: #17 (function of type var"#17#21")

Ahora inicializamos un segundo contenedor cambiando sólo el parámetros \(\alpha\) por 0.5:

modelo2 = Modelo(α=0.50)
Modelo
  α: Float64 0.5
  β: Float64 0.99
  u: #6 (function of type var"#6#10")

Finalmente, inicialiamos un tercer contenedos con la función \(\exp(x)\) y el parámetro \(\beta=0.9\):

g(x) = exp(x)
modelo3 = Modelo(u = g, β = 0.90)  # note que el orden no importa, es el nombre lo relevante
Modelo
  α: Float64 0.33
  β: Float64 0.9
  u: g (function of type typeof(g))

Como antes, es posible acceder a la información dentro de cada conenedor usando el formato: contenedor.objeto. Por ejemplo:

modelo3.u(1.5)
4.4816890703380645

También es posible desempaquetar cada contenedor usando la macro @unpak:

@unpack α, u = modelo3;
@show α
@show u(1.5);
α = 0.33
u(1.5) = 4.4816890703380645