À 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
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.
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.
+ est une fonction qui prend 161 méthodes, actuellement).... puisse en partie contourner cette règle).dispatched avec les arguments donnés, et renvoie un résultat.MethodError est levée.À noter que Julia est typé dynamiquement, donc le dispatch des méthodes se fait au moment de l'exécution.
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
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)
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 !
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.
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.
Implémente une fonction name() qui renvoie le nom d'un animal de compagnie.
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
Implémente les méthodes meets() pour les rencontres suivantes :
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
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."
Que se passe-t-il s'ils croisent un autre animal pendant leur promenade, comme un cheval ?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
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 ?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
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.
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.
Inscris-toi sur Exercism pour apprendre et maîtriser Julia avec 35 concepts128 exercices, et un vrai mentorat humain, le tout gratuitement.