A estas alturas del temario, ya has visto muchas definiciones de funciones. Por lo general, se presentaban sin muchos comentarios sobre 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 agregan 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 de alguna manera sabe 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 mapea una tupla de argumentos a un valor de retorno.
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 es cierto incluso para un solo argumento (aunque hay que agregar 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 de retorno. De forma más formal, la tupla se mapea a un valor de retorno.
A esto se le suele llamar «polimorfismo».
Este ha sido un tema importante en las ciencias de la computación durante varias décadas, y se han adoptado distintos enfoques.
El enfoque de Julia es poco común, aunque no único.
+ es una función con 161 métodos, actualmente).... puede saltarse esto parcialmente).dispatched con los argumentos dados y devuelve un resultado.MethodError.Ten en cuenta que Julia tiene tipado dinámico, así que el despacho de métodos ocurre en tiempo de ejecución.
La sección anterior tuvo muchas palabras, así que traduzcamos esas ideas a código. Por simplicidad, las funciones siguientes tienen solo un argumento, pero no es raro encontrar funciones con docenas de argumentos (por ejemplo, en paquetes de gráficos o de aprendizaje automático).
Empieza por definir 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 firmas de los métodos pueden tomar valores de tipo ficticios, más o menos de la misma manera 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 requiere que ambos argumentos usen el mismo tipo T, cualquiera que este 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.
Es posible tener 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 de alto rendimiento en Julia, por lo que se usa ampliamente.
Como sugiere el ejemplo de los 161 métodos para +, quienes programan en Julia buscan definir métodos con tipos especificados de forma estrecha.
Sumar dos enteros es muy distinto de sumar dos números de punto flotante a nivel de bytecode, y sumar un entero a un número de punto flotante agrega una complicación adicional: la promoción de tipos.
Cuanto más específico sea el tipo, más optimizaciones puede hacer el compilador: de importancia marginal en un ejercicio sencillo en Exercism, pero vital para el rendimiento en las grandes simulaciones numéricas para las que Julia está diseñada.
Si sabes qué argumentos va a recibir un método, ¡díselo al compilador!
En este ejercicio vas a implementar una simulación de encuentros. Esto te familiarizará con lo básico 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, los encuentros involucran a una entidad a que se encuentra con otra entidad b y reacciona ante ella.
encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."
Al principio, simularemos qué pasa cuando se encuentran gatos y perros.
Primero, agrega un tipo abstracto Pet.
Luego, define los tipos Cat y Dog como subtipos de Pet.
Cada uno tiene un solo 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 de 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 reportar qué pasa cuando se encuentran dos entidades con nombre.
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
¿Qué pasa si se encuentran con otra mascota durante su paseo, 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 una mascota puede encontrarse y que no son mascotas: autos, humanos, plantas, desastres naturales, asteroides… ¿Qué pasa entonces?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
Hay muchos otros encuentros que podrían ocurrir en nuestra simulación. Un auto se encuentra con un perro, dos electrones se encuentran e interactúan…
Es imposible cubrir todos los encuentros de antemano, así que implementaremos un respaldo genérico.
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.