Rutas
/
Julia
Julia
/
Ejercicios
/
Encuentros
Encuentros

Encuentros

Ejercicio de aprendizaje

Introducción

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

¿Qué es una función?

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.

¿Cómo gestionan las funciones distintos números y tipos de argumentos?

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.

  • Las funciones son «genéricas», lo que significa que pueden tener varios métodos asociados.
  • Las funciones son solo nombres hasta que se les asocian métodos.
  • Se pueden añadir métodos según se necesiten, potencialmente en grandes cantidades (el operador + es una función con 161 métodos, actualmente).
  • Cuando se llama a una función, Julia examina la signatura de la llamada: el número de argumentos y el tipo actual de cada uno.
  • Se selecciona el método que mejor coincide con la signatura de la llamada.
    • En general, el número de argumentos debe coincidir exactamente (aunque el operador splat/slurp ... puede saltarse esto en parte).
    • Se prefieren los métodos cuyos tipos coinciden exactamente con la signatura de la llamada.
    • Si es necesario, Julia examinará los supertipos de cada argumento de la signatura de la llamada para encontrar el método más específico y que mejor coincida.
  • El método elegido se dispatched con los argumentos dados y devuelve un resultado.
  • Si no se encuentra ningún método adecuado, se lanza un MethodError.

Ten en cuenta que Julia tiene tipado dinámico, por lo que el despacho de métodos ocurre en tiempo de ejecución.

Despacho múltiple: un ejemplo

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

Métodos paramétricos

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)

Conclusión

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!

Instrucciones

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.

1. Define tipos personalizados

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.

2. Obtén el nombre de la mascota.

Implementa una función name() que devuelva el nombre de una mascota.

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. Define qué ocurre cuando se encuentran gatos y perros

Implementa los métodos meets() para los siguientes encuentros:

  • Cuando se encuentran dos perros, se sniff el uno al otro.
  • Cuando un gato se encuentra con un perro, le hisses al perro.
  • Cuando un perro se encuentra con un gato, chases al gato.
  • Cuando se encuentran dos gatos, slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. Define un encuentro entre dos entidades.

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."

5. Define una reacción de reserva para los encuentros entre mascotas

¿Qué ocurre si durante su paseo se topan con otra mascota, como un caballo?

  • Si una mascota se encuentra con otra mascota que no reconoce, is cautious.
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. Define una reserva por si una mascota se topa con algo que no conoce

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?

  • Si una mascota se encuentra con algo que nunca ha visto antes, runs away.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. Define una reserva genérica

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.

  • Si dos cosas o seres desconocidos se encuentran, nothing happens.
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.
Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Julia Exercism

¿Listo para empezar Encuentros?

Regístrate en Exercism para aprender y dominar Julia con 35 conceptos128 ejercicios y mentoría humana real, todo gratis.