A estas alturas del temario, ya has visto muchas definiciones de funciones. Normalmente se presentaban sin apenas comentar la salida en el REPL:
julia> f(x) = x^2
f (generic function with 1 method)
¿Por qué "generic function with 1 method" ?
Supongamos que queremos ser más específicos con los tipos de los argumentos.
julia> f(x::Integer) = x^2
f (generic function with 2 methods)
julia> f(x::Real) = x^2
f (generic function with 3 methods)
O podríamos cambiar el número de argumentos:
julia> f(x, y) = x * y
f (generic function with 4 methods)
Parece que las nuevas definiciones de la misma función se llaman «métodos». Los métodos se añaden a una lista en lugar de sobrescribir la definición anterior.
Podemos listar los métodos fácilmente:
julia> methods(f) # output edited for brevity
# 4 methods for generic function "f" from Main:
[1] f(x, y)
[2] f(x::Integer)
[3] f(x::Real)
[4] f(x)
Si llamamos a la función f, Julia sabe de algún modo qué método usar:
julia> f(2.5) # Float64
6.25
julia> f(2) # Int64
4
julia> f(2, 3) # Int64, Int64
6
Según el manual, una función es un objeto que asigna una tupla de argumentos a un valor devuelto.
Normalmente pensamos en los argumentos de la función en f(2, 3) como dos elementos separados, pero el compilador los ve como una sola tupla. Esto también ocurre con un único argumento (aunque hay que añadir una coma para que quede más claro):
julia> typeof( (2, 3) )
Tuple{Int64, Int64}
julia> typeof( (2,) )
Tuple{Int64}
Si vemos la función como una caja negra, podemos decir que entra una tupla y sale un valor devuelto. Más formalmente, la tupla se asigna a un valor devuelto.
A esto se le suele llamar «polimorfismo».
Ha sido un tema importante en la informática durante varias décadas y se han adoptado distintos enfoques. El enfoque de Julia es poco habitual, aunque no único.
+ es una función con 161 métodos, actualmente).... puede saltarse esto en parte).dispatched con los argumentos dados y devuelve un resultado.MethodError.Ten en cuenta que Julia tiene tipado dinámico, por lo que el despacho de métodos ocurre en tiempo de ejecución.
La sección anterior tenía muchas palabras, así que vamos a llevar esas ideas al código. Para simplificar, las funciones de abajo solo tienen un argumento, pero no es raro encontrar funciones con decenas de argumentos (como en los paquetes de gráficos o en el aprendizaje automático).
Empecemos definiendo algunos tipos personalizados para representar figuras geométricas.
julia> abstract type Shape end
julia> struct Circle <: Shape
radius::Float64
end
julia> struct Square <: Shape
side::Float64
end
julia> struct Rectangle <: Shape
length::Float64
width::Float64
end
A continuación podemos definir métodos para calcular el área de cada figura. Cada uno recibe un argumento, de un tipo concreto definido por el usuario distinto en cada caso.
julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)
julia> area(s::Square) = s.side^2
area (generic function with 2 methods)
julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)
Ahora podemos crear algunas figuras y calcular sus áreas, confiando en el mecanismo de despacho de Julia para usar la fórmula correcta en cada caso.
julia> circle = Circle(2.3)
Circle(2.3)
julia> area(circle) # call signature Tuple{Circle}
16.619025137490002
julia> square = Square(1.6)
Square(1.6)
julia> area(square) # call signature Tuple{Square}
2.5600000000000005
julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)
julia> area(rectangle) # call signature Tuple{Rectangle}
2.94
Las signaturas de los métodos pueden tomar valores de tipo ficticios, más o menos igual que los tipos paramétricos.
La sintaxis es peculiar, ya que requiere una cláusula where:
julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type([1, 2], 3)
true
julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)
El ejemplo anterior exige que ambos argumentos usen el mismo tipo T, sea cual sea.
Podemos ser más específicos imponiendo una restricción a T:
julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)
Con este método, solo los tipos numéricos idénticos devuelven true.
Se pueden usar varias variables ficticias, separadas por comas en la cláusula where.
julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)
El despacho múltiple es muy flexible y muy eficiente en Julia, por lo que se usa muchísimo.
Como sugiere el ejemplo de los 161 métodos de +, quienes programan en Julia procuran definir métodos con tipos muy específicos. Sumar dos enteros es muy distinto de sumar dos números de coma flotante a nivel de bytecode, y sumar un entero a un número de coma flotante añade la complicación adicional de la promoción de tipos.
Cuanto más específico es el tipo, más optimizaciones puede hacer el compilador: algo de importancia marginal en un ejercicio de juguete en Exercism, pero vital para el rendimiento en las grandes simulaciones numéricas para las que Julia está diseñada.
Si sabes qué entradas va a recibir un método, ¡avísale al compilador!
En este ejercicio vas a implementar una simulación de encuentros. Esto te familiarizará con los conceptos básicos del despacho múltiple, el paradigma principal de Julia.
For this exercise, all function definitions should be a single-line declaration with no if/else logic.
En general, un encuentro consiste en que una entidad a se topa con otra entidad b y reacciona ante ella.
encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."
Para empezar, vamos a simular qué ocurre cuando se encuentran gatos y perros.
Primero, añade un tipo abstracto Pet.
Después, define los tipos Cat y Dog como subtipos de Pet.
Cada uno tiene un único campo name.
Implementa una función name() que devuelva el nombre de una mascota.
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
Implementa los métodos meets() para los siguientes encuentros:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
Implementa la función encounter() con un método, para informar de qué ocurre cuando se encuentran dos entidades con nombre.
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
¿Qué ocurre si durante su paseo se topan con otra mascota, como un caballo?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
Hay muchas otras cosas con las que las mascotas pueden encontrarse y que no son mascotas: coches, humanos, plantas, desastres naturales, asteroides… ¿Qué ocurre entonces?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
Hay muchos otros encuentros que podrían darse en nuestra simulación. Un coche se encuentra con un perro, dos electrones se topan e interactúan…
Es imposible cubrir todos los encuentros de antemano, así que implementaremos una reserva genérica.
julia> γ1 = Photon("γ1")
Photon("γ1")
julia> γ2 = Photon("γ2")
Photon("γ2")
julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens."
# This is true, photons just pass through each other.
Regístrate en Exercism para aprender y dominar Julia con 35 conceptos128 ejercicios y mentoría humana real, todo gratis.