Skip to the content.

Jugando al multiple dispatch

Recordemos algunos experimentos con multiple dispatch.

  julia> h(x) = println("Esta es la función por defecto") 
  julia> h(x::Number) = println("Recibí un número")
  julia> h(x::String) = println("Recibí una cadena de caracteres")
  julia> h(2)
  julia> h(2.5)
  julia> h(2//3)
  julia> h("palabra")
  julia> h([1,2,3])
  julia> h((1,2))
  julia> h(1,2)
  julia> h(x::Rational) = println("Recibí un número racional")
  julia> h(3//4)
  julia> h(2.5)

Notemos:

Supertipos y subtipos

Ya vimos que el sistema de multiple dispatch es capaz de elegir qué método usar en función de la combinación de tipos de los parámetros. Vimos también que además de Int64 y Float64 existen tipos como Number. Exploremos un poco esto:

  julia> T = typeof(3)
  julia> supertype(T)
  julia> subtypes(T)
  julia> subtypes(supertype(T))

Vemos que Int64 tiene como supratipo a Signed y no tiene subtipos. Los subtipos de Signed son BigInt, Int128, Int64, Int32, Int16, Int8.

Continuemos investigando hacia arriba:

  julia> for i in 1:7
            println(T)
            T = supertype(T)
         end  

Vemos una secuencia de tipos que llega a Any y se estanca allí. Es decir: Any se tiene por supratipo a sí mismo. Con esto en mente podemos hacer una función que haga lo que hicimos recién:

function supertipos(T)
    sup = [T]
    while T != Any
        T = supertype(T)
        push!(sup,T)
    end
    for i in length(sup):-1:1
        println(" "^(length(sup)-i),sup[i])
    end
end

El operador ^ aplicado a un String lo reitera tantas veces como indica la potencia. Es decir que " "^(length(sup)-1) pone una cantidad de espacios delante del tipo (empezando por ninguno y agregando uno en cada iteración).

Usar esta función para explorar la cadena de tipos que conduce a alguno de los tipos conocidos: Float64, Bool, Int64, UInt64, String.

Observamos que todos los tipos numéricos derivan de Real, pero el único antepasado común de éstos con String es Any.

De manera similar podemos hacer una exploración hacia abajo, mirando los subtipos.

function subtipos(T,nivel=0)
    println("  "^nivel,T)
    for S in subtypes(T)
        subtipos(S,nivel+1)
    end
end
Recursividad: Acabamos de implementar nuestra primera función recursiva. subtipos se llama a sí misma para calcular los subtipos de los subtipos, etc. El parámetro opcional nivel nos sirve para indicar la cantidad de espacios que usamos y nos permite mostrar la información de manera que se vea quién es subtipo de quién.

Probemos nuestra función calculando los subtipos del misterioso Number. Vemos que de Number se desprenden Complex y Real y de Real derivan distintas variantes de flotantes y enteros, los racionales y … los irracionales. A modo de ejemplo, probar:

  julia> sqrt(2)
  julia> π

Julia sabe que π es irracional. Al menos en principio, no sabe que la raíz de dos también.

Calculemos también los subtipos de AbstractVector. Si se animan, calculen los subtipos de Any (se recomienda cerrar y abrir Julia antes de hacer esta operación, para mostrar sólo los tipos incluidos en la instalación básica del lenguaje). Advertencia: esto puede llevar mucho tiempo.

La estructura de tipos de Julia

Las relaciones entre los tipos pueden constatarse con una sintaxis muy elegante:

  julia> Int64 <: Signed
  julia> Int32 <: Number
  julia> Int64 <: Float64
  julia> Int8 <: Int16
  julia> Integer :> Int16
  julia> Integer <: AbstractFloat
  julia> Integer <: Number
Programación genérica: Esta estructura del sistema de tipos permite escribir programas muy genéricos. Por ejemplo, supongamos que implementamos una función que recibe un número decimal y realiza ciertas operaciones. Es conveniente implementarla indicando que el parámetro es de tipo AbstractFloat. Esto permitirá que el compilador optimice el código en cada caso, según el usuario use la función con Float64, Float32 u otra variante de AbstractFloat. Pero no sólo eso. Julia tiene la librería Decimals.jl que implementa un arquitectura para decimales arbitrarios. Allí, el tipo Decimal se define como suptipo de AbstractFloat. Es decir que nuestra función correrá automáticamente y sin que nosotros hagamos nada si el usuario la corre sobre un dato de tipo Decimal. Esto vale para cualquier tipo de dato. Las distintas librerías de Julia suelen interactuar perfectamente entre sí sin que haya que hacer nada específico para combinarlas. Incluso cuando los desarrolladores de una librería ignoraban por completa la existencia de la otra y viceversa.

Tipos paramétricos

Considerar lo siguiente:

  julia> x = 2//3
  julia> typeof(x)
  julia> y = Rational(2,3)
  julia> typeof(y)
  julia> z = Rational(UInt8(2),UInt8(3))
  julia> typeof(z)
  julia> w = Rational{Int16}(2,3)
  julia> typeof(w)
  julia> c = 2 + 4im
  julia> d = 2.5 + 2im

Estos ejemplos nos muestran que Rational y Complex no son tipos concretos en el sentido de tener una representación de máquina predeterminada. El tipo de racional que definimos depende del tipo de enteros que usamos para el numerador y el denominador. Algo similar ocurre con la parte real y compleja de los complejos.

Por ejemplo, un racional definido de manera estándar (2//3) es de tipo Rational{Int64}. Es decir que tenemos un tipo paramétrico. Exploremos esto: si usamos @edit o @less para ver cómo se define un racional vemos lo siguiente:

struct Rational{T<:Integer} <:Real
   num::T
   den::T
end

(omitimos sólo una línea que contiene un constructor para Rational).

¿Qué nos dice esto?

De esto podemos inferir algunas conclusiones iniciales:

Consideremos lo siguiente:

  julia> Rational{Int64} <: Rational
  julia> Rational{UInt16} <: Rational
  julia> Rational{Signed} <: Rational
  julia> Rational{Int64} <: Rational{Signed}

Es decir: Rational{T} para cualquier tipo T es un subtipo de Rational. Pero Rational{Int64} no es un subtipo de Rational{Signed} pese a que Int64 es un subtipo de Signed. Es decir: las relaciones de parentesco no se anidan.

Rational es lo que se llama un tipo UnionAll. Es decir funciona como la unión de Rational{T} para todo T.

Unión de tipos

En algunos casos puede resultar útil admitir tipos de datos disímiles. Por ejemplo, supongamos que queremos generar una estructura de datos que adentro tendrá un flotante, pero queremos dejar la posibilidad de que ese valor quede sin inicializar. En tal caso, le asignaríamos el valor nothing, cuyo tipo es Nothing (que desciende directamente de Any). Esto lo podemos lograr haciendo:

struct MiDato{T<:Union{AbstractFloat,Nothing}}
  num::T
end

Es decir que MiDato es un contenedor que tiene un numero y ese número puede ser cualquier variante de flotante (descendiente de AbstractFloat) o nothing (único valor de tipo Nothing).

El sistema de multiple dispatch es muy eficiente y maneja bien uniones, siempre que sean de pocos tipos.

Explorando tipos compuestos

Ahora que conocemos los entretelones del tipo Rational, probemos algunas cosas:

  julia> r = 2//3
  julia> propertynames(r)
  julia> r.num
  julia> r.den
  julia> getproperty(r,:den)
  julia> getproperty(r,:num)
  julia> denominator(r)
  julia> numerator(r)


Volver a la primera parte
Ir a la clase 4