Rencontres

Rencontres

Exercice d'apprentissage

Introduction

À ce stade du programme, tu as déjà vu beaucoup de définitions de fonctions. En général, elles étaient présentées sans grande explication sur la sortie dans le REPL :

julia> f(x) = x^2
f (generic function with 1 method)

Pourquoi "generic function with 1 method" ?

Supposons que l'on veuille être plus précis sur les types des arguments.

julia> f(x::Integer) = x^2
f (generic function with 2 methods)

julia> f(x::Real) = x^2
f (generic function with 3 methods)

Ou on pourrait changer le nombre d'arguments :

julia> f(x, y) = x * y
f (generic function with 4 methods)

Il semble que de nouvelles définitions de la même fonction soient appelées des « méthodes ». Les méthodes s'ajoutent à une liste, au lieu d'écraser l'ancienne définition.

On peut facilement lister les méthodes :

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 on appelle la fonction f, Julia sait d'une manière ou d'une autre quelle méthode utiliser :

julia> f(2.5)  # Float64
6.25

julia> f(2)  # Int64
4

julia> f(2, 3)  # Int64, Int64
6

Qu'est-ce qu'une fonction ?

D'après le manuel, une fonction est un objet qui associe un tuple d'arguments à une valeur de retour.

On considère normalement les arguments de la fonction dans f(2, 3) comme deux éléments distincts, mais le compilateur les voit comme un seul tuple. C'est vrai même pour un seul argument (même s'il faut ajouter une virgule pour que ce soit plus clair) :

julia> typeof( (2, 3) )
Tuple{Int64, Int64}

julia> typeof( (2,) )
Tuple{Int64}

Si on considère la fonction comme une boîte noire, on peut dire qu'un tuple entre et qu'une valeur de retour sort. Plus formellement, le tuple est associé à une valeur de retour.

Comment les fonctions gèrent-elles différents nombres et types d'arguments ?

On parle généralement de « polymorphisme ».

C'est un vaste sujet en informatique depuis plusieurs décennies, et différentes approches ont été adoptées.

L'approche de Julia est inhabituelle, sans être unique.

  • Les fonctions sont « génériques », ce qui signifie qu'on peut leur attacher plusieurs méthodes.
  • Les fonctions ne sont que des noms, tant qu'aucune méthode ne leur est attachée.
  • On peut ajouter des méthodes selon les besoins, potentiellement en grand nombre (l'opérateur + est une fonction qui prend 161 méthodes, actuellement).
  • Quand une fonction est appelée, Julia examine la signature d'appel : le nombre d'arguments et le type courant de chacun.
  • La méthode qui correspond le mieux à la signature d'appel est sélectionnée.
    • En général, le nombre d'arguments doit correspondre exactement (bien que l'opérateur splat/slurp ... puisse en partie contourner cette règle).
    • Les méthodes dont les types correspondent exactement à la signature d'appel sont privilégiées.
    • Si nécessaire, Julia examine les supertypes de chaque argument de la signature d'appel pour trouver la méthode la plus spécifique et la plus proche.
  • La méthode choisie est dispatched avec les arguments donnés, et renvoie un résultat.
  • Si aucune méthode appropriée n'est trouvée, une MethodError est levée.

À noter que Julia est typé dynamiquement, donc le dispatch des méthodes se fait au moment de l'exécution.

Le dispatch multiple : un exemple

La section précédente contenait beaucoup de mots ; traduisons donc ces idées en code. Par souci de simplicité, les fonctions ci-dessous ne prennent qu'un seul argument, mais il n'est pas rare de trouver des fonctions qui prennent des dizaines d'arguments (comme dans les paquets de tracé ou d'apprentissage automatique).

Commençons par définir quelques types personnalisés pour représenter des formes géométriques.

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

Ensuite, on peut définir des méthodes pour calculer l'aire de chaque forme. Chacune prend un argument, d'un type concret défini par l'utilisateur différent dans chaque cas.

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)

On peut maintenant créer quelques formes et calculer les aires, en s'appuyant sur le mécanisme de dispatch de Julia pour utiliser la bonne formule pour chacune.

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éthodes paramétriques

Les signatures de méthodes peuvent prendre des valeurs de type muettes, à peu près de la même manière que les types paramétriques.

La syntaxe est particulière et nécessite une clause 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)

L'exemple ci-dessus exige que les deux arguments utilisent le même type T, quel qu'il soit.

On peut être plus précis en imposant une contrainte sur T :

julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)

Avec cette méthode, seuls des types numériques identiques renvoient true.

Il est possible d'avoir plusieurs variables muettes, séparées par des virgules dans la clause where.

julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)

Conclusion

Le dispatch multiple est très souple et très performant en Julia, il est donc très largement utilisé.

Comme le suggère l'exemple des 161 méthodes de +, les développeurs Julia cherchent à définir des méthodes aux types précisément spécifiés. Additionner deux entiers est très différent d'additionner deux nombres à virgule flottante au niveau du bytecode, et additionner un entier à un nombre à virgule flottante ajoute une complication supplémentaire : la promotion de type.

Plus le type est spécifique, plus le compilateur peut optimiser : d'une importance marginale dans un petit exercice sur Exercism, mais essentiel pour la performance dans les grandes simulations numériques pour lesquelles Julia est conçu.

Si tu sais quelles entrées une méthode va recevoir, dis-le au compilateur !

Instructions

Dans cet exercice, tu vas implémenter une simulation de rencontres. Cela te fera découvrir les bases du dispatch multiple, le paradigme principal de Julia.

For this exercise, all function definitions should be a single-line declaration with no if/else logic.

De manière générale, une rencontre consiste en une entité a qui en croise une autre, b, et qui réagit à elle.

encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."

Dans un premier temps, on va simuler ce qui se passe quand des chats et des chiens se rencontrent.

1. Définis des types personnalisés

Ajoute d'abord un type abstrait Pet.

Définis ensuite les types Cat et Dog comme sous-types de Pet. Chacun possède un seul champ name.

2. Récupère le nom de l'animal de compagnie.

Implémente une fonction name() qui renvoie le nom d'un animal de compagnie.

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

julia> name(fido)
"Fido"

3. Définis ce qui se passe quand des chats et des chiens se rencontrent

Implémente les méthodes meets() pour les rencontres suivantes :

  • Quand deux chiens se rencontrent, la réaction est sniff.
  • Quand un chat rencontre un chien, la réaction est hisses.
  • Quand un chien rencontre un chat, la réaction est chases.
  • Quand deux chats se rencontrent, la réaction est slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. Définis une rencontre entre deux entités.

Implémente la fonction encounter() avec une seule méthode, pour rapporter ce qui se passe quand deux entités nommées se rencontrent.

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

5. Définis une réaction de repli pour les rencontres entre animaux

Que se passe-t-il s'ils croisent un autre animal pendant leur promenade, comme un cheval ?

  • Si un animal croise un autre animal qu'il ne reconnaît pas, la réaction est is cautious.
julia> bella = Horse("Bella")
Horse("Bella")

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

6. Définis un repli si un animal croise quelque chose qu'il ne connaît pas

Il y a beaucoup d'autres choses que les animaux peuvent croiser et qui ne sont pas des animaux : des voitures, des humains, des plantes, des catastrophes naturelles, des astéroïdes… Que se passe-t-il alors ?

  • Si un animal croise quelque chose qu'il n'a jamais vu auparavant, la réaction est runs away.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

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

7. Définis un repli générique

Bien d'autres rencontres peuvent avoir lieu dans notre simulation. Une voiture croise un chien, deux électrons se rencontrent et interagissent…

Il est impossible de prévoir toutes les rencontres à l'avance, c'est pourquoi on va implémenter un repli générique.

  • Si deux choses ou êtres inconnus se rencontrent, la réaction est 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.
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
Julia Exercism

Prêt à commencer Rencontres ?

Inscris-toi sur Exercism pour apprendre et maîtriser Julia avec 35 concepts128 exercices, et un vrai mentorat humain, le tout gratuitement.