Tracks
/
Julia
Julia
/
Ejercicios
/
Encuentros
Encuentros

Encuentros

Ejercicio de aprendizaje

Introducción

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

¿Qué es una función?

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.

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

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.

  • Las funciones son «genéricas», lo que significa que pueden tener varios métodos asociados.
  • Las funciones son solo nombres, hasta que tienen métodos asociados.
  • Los métodos se pueden agregar según sea necesario, 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 firma de la llamada: el número de argumentos y el tipo actual de cada uno.
  • Se selecciona el método que coincide más estrechamente con la firma de la llamada.
    • Por lo general, el número de argumentos debe coincidir exactamente (aunque el operador splat/slurp ... puede saltarse esto parcialmente).
    • Se prefieren los métodos cuyos tipos coinciden exactamente con la firma de la llamada.
    • Si es necesario, Julia examinará los supertipos de cada argumento de la firma de la llamada para encontrar el método más específico que coincida más estrechamente.
  • El método elegido se dispatched con los argumentos dados y devuelve un resultado.
  • Si no se encuentra un método adecuado, se lanza un MethodError.

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

Despacho múltiple: un ejemplo

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

Métodos paramétricos

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)

Conclusión

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!

Instrucciones

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.

1. Define tipos personalizados

Primero, agrega un tipo abstracto Pet.

Luego, define los tipos Cat y Dog como subtipos de Pet. Cada uno tiene un solo 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é pasa cuando se encuentran gatos y perros

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

  • Cuando dos perros se encuentran, 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 dos gatos se encuentran, 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 reportar qué pasa cuando se encuentran dos entidades con nombre.

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

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

¿Qué pasa si se encuentran con otra mascota durante su paseo, 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 un respaldo en caso de que una mascota se encuentre con algo que no conoce

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?

  • 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 un respaldo genérico

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.

  • 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 una pestaña nuevas
Julia Exercism

¿Todo listo para empezar Encuentros?

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