Pour commencer, nous allons voir en profondeur les deux types de paramètres des méthodes de Ruby. Comment et quand les utiliser ? Quels problèmes peuvent se poser durant leur utilisation ? Nous allons répondre à ces questions ici.
Rappels
Pour démarrer sur de bonnes bases, faisons quelques petits rappels et posons les bases de notre travail.
Arguments ou paramètres ?
Pour poursuivre les rappels, il nous faut faire un point sur les notions d’arguments et de paramètres. Généralement, les deux mots sont employés de manière indifférenciée, mais nous allons cependant apporter une petite nuance à cela. Dans un tutoriel qui traite en profondeur des méthodes et des arguments, cette nuance semble indispensable et nous permettra de toujours bien savoir ce dont nous parlons, sans aucune ambiguïté.
Les paramètres sont les noms que nous utilisons lorsque nous définissons une méthode et les arguments sont les valeurs passées à la méthode lors de son appel.
def introduce(first_name, last_name, age)
"#{first_name} #{last_name} a #{age} ans."
end
introduce('Mickey', 'Mouse', 92)
introduce('Balthazar', 'Picsou', '73')
Dans cet exemple, la méthode introduce
a trois paramètres, first_name
, last_name
et age
et nous l’appelons deux fois avec des arguments différents. La première fois avec les arguments 'Mickey'
, 'Mouse'
et 92
, et la seconde fois avec les arguments 'Balthazar'
, 'Picsou'
et '73'
.
Lorsque nous appelons une méthode, nous lui donnons des arguments, et nous récupérons les valeurs de ces arguments dans les paramètres de ladite méthode. Par exemple, avec notre premier appel à introduce
, nous récupérons la valeur 'Mickey'
dans la variable first_name
, Mouse
dans la variable last_name
et 92
dans la variable age
. Les paramètres sont donc les variables dans lesquelles on récupère les arguments donnés à la méthode lors d’un appel.
Dans certains cas, nous entendrons également parler de « paramètre formel » pour désigner les paramètres et de « paramètre effectif » pour les arguments.
Les gens qui font un peu de théorie (par exemple de la logique, du lambda-calcul et pleins d’autres gros mots) ont la notion de variable liée qui correspond à la notion de paramètre d’une fonction. En effet, lorsque l’on appelle une méthode f
de paramètre x
avec un argument v
, on peut se dire que l’on lie la valeur v
à la variable x
dans la méthode (la variable x
a désormais la valeur v
).
Finalement, parler des paramètres d’une méthode a du sens là où parler des arguments d’une méthode n’en a pas. En effet, une méthode n’a pas, à proprement parler, d’arguments. C’est seulement lorsque nous appelons la méthode que le mot argument a du sens : nous appelons la méthode avec des arguments.
Les paramètres positionnels
Les paramètres positionnels correspondent aux paramètres classiques que l’on connaît. Lorsqu’on appelle une méthode, Ruby sait dans quel paramètre il faut récupérer chaque argument grâce à leur position. Reprenons l’exemple précédent.
def introduce(first_name, last_name, age)
"#{first_name} #{last_name} a #{age} ans."
end
introduce('Mickey', 'Mouse', 92)
Ici,introduce
a trois paramètres positionnels. Lorsque la méthode est appelée, Ruby sait alors que le premier argument doit être récupéré dans le paramètre first_name
, le deuxième dans le paramètre last_name
, et le dernier dans le paramètre age
. C’est bien l’ordre dans lequel les arguments sont passés, leur position qui indique à Ruby dans quel paramètre les récupérer.
Si nous appelons la méthode avec les mêmes arguments, mais dans un autre ordre, le résultat sera donc différent.
introduce('Mouse', 92, 'Mickey')
# => 'Mouse 92 a Mickey ans.
Valeurs par défaut
Ruby nous permet de donner une valeur par défaut à certains paramètres. Lorsque nous appelons la méthode, nous pouvons alors ne pas donner d’argument pour ce paramètre. Ici, nous allons avoir un paramètre pour choisir si le nom doit être affiché avant ou après le prénom.
def introduce(first_name, last_name, age, reverse = false)
return "#{last_name} #{first_name} a #{age} ans." if reverse
"#{first_name} #{last_name} a #{age} ans."
end
introduce('Mickey', 'Mouse', 92)
introduce('Mickey', 'Mouse', 92, true)
Dans le premier appel, nous n’avons pas fourni de valeur pour le dernier paramètre reverse
, sa valeur était donc celle par défaut, à savoir false
. Dans le second appel, nous avons fourni la valeur true
, ce qui nous a permis d’obtenir une chaîne formatée différemment.
Dans notre exemple, nous avons une valeur par défaut, mais nous pourrions en mettre plusieurs. Rajoutons par exemple un argument location
avec comme valeur par défaut 'Mickeyville'
(si nous supposons écrire un programme sur l’univers de Mickey, il est raisonnable d’avoir Mickeyville comme ville par défaut.
def introduce(first_name, last_name, age, location = 'Mickeyville', reverse = false)
return "#{last_name} #{first_name} a #{age} ans et habite à #{location}." if reverse
"#{first_name} #{last_name} a #{age} ans et habite à #{location}."
end
introduce('Mickey', 'Mouse', 92)
introduce('Donald', 'Duck', 86, 'Donaldville')
introduce('Donald', 'Duck', 86, 'Donaldville', true)
Cette fois-ci, nous avons trois exemples à examiner. Dans le premier cas, nous ne fournissons de valeur ni pour location
, ni pour reverse
, les valeurs par défaut sont prises. Dans le deuxième cas, nous fournissons une valeur, 'Donaldville'
en quatrième argument. Cette valeur est récupérée dans le paramètre location
et reverse
garde la valeur par défaut. Et finalement, dans le troisième cas, nous fournissons 'Donaldville'
et true
en quatrième et cinquième arguments, qu’on récupérera respectivement dans les quatrième et cinquième paramètre, location
et reverse
.
Bien sûr, le nombre d’arguments donnés à la méthode doit être supérieur ou égal au nombre de paramètres sans valeur par défaut de cette méthode, et doit être inférieur ou égal au nombre de paramètres total de cette méthode. Ainsi, introduce
doit être appelée avec un nombre d’arguments compris entre 3
et 5
.
Nous pouvons faire un peu plus de choses avec les valeurs par défaut des paramètres positionnels, ce que nous verrons plus loin.
Un problème à régler
À chaque problème…
Lorsque nous avons plusieurs valeurs par défaut comme dans le dernier code que nous avons considéré, nous ne pouvons pas faire tous les appels que nous voudrions possiblement faire.
def introduce(first_name, last_name, age, location = 'Mickeyville', reverse = false)
return "#{last_name} #{first_name} a #{age} ans et habite à #{location}." if reverse
"#{first_name} #{last_name} a #{age} ans et habite à #{location}."
end
Nous pouvons faire trois types d’appels avec cette méthode.
- Ne pas donner de valeur pour
location
etreverse
. - Donner de valeur pour
location
et ne pas en donner pourreverse
. - Donner une valeur pour
location
et donner une valeur pourreverse
.
Mais il y a un type d’appel que nous ne pouvons pas faire : nous ne pouvons pas donner de valeur pour reverse
sans donner de valeur pour location
! En effet, ce sont les positions des arguments qui indiquent dans quel paramètre ils sont récupérés et appeler introduce
avec quatre arguments nous ferait donc récupérer le quatrième argument dans location
.
Dans certains langages, on contourne ce problème en indiquant lors de l’appel de la méthode le nom du paramètre que l’on veut associer à la valeur, de la manière suivante.
# On précise que l'argument `true` est pour le paramètre `reverse`.
introduce('Mickey', 'Mouse', 92, reverse = true)
En Ruby, cela ne fonctionne pas. En fait, lorsque Ruby lit ce code, il comprend que nous voulons faire l’affectation reverse = true
et donner le résultat de cette affectation à la méthode. Et il se trouve que le résultat d’une affectation est le résultat de l’expression que l’on affecte (donc dans notre cas, true
). Finalement, ce code donne à reverse
la valeur true
et appelle introduce
avec true
comme quatrième argument.
# reverse n'est pas encore définie
introduce('Mickey', 'Mouse', 92, reverse = true)
# => 'Mickey Mouse a 92 ans et habite à true.'
reverse
# => true
C’est une mécanique de Ruby intéressante qui peut être utilisée pour définir ou modifier des variables lors de l’appel d’une méthode.
Il ne faut cependant pas en abuser. Notre but est d’avoir un code clair et lisible.
def f(x, y, z, t)
0
end
x = 0
f(x = 3, x += 2, x = 4, x -= 1)
# Que vaut x ?
Le code précédent n’est certainement pas le plus lisible qui soit. Nous pouvons trouver la valeur de x
car les arguments sont évalués dans l’ordre. Ainsi, on fait d’abord x = 3
, puis x += 2
, x = 4
, et enfin x -= 1
. Donc x
vaut 3
.
Sa solution !
La solution à ce problème est un deuxième type de paramètres de Ruby : les paramètres nommés. Il s’agit de paramètres dont nous devons préciser le nom à l’appel de la méthode. Ruby utilisera alors ce nom, plutôt que la position de l’argument, pour déterminer dans quel paramètre cet argument doit être récupéré. Les paramètres nommés s’écrivent avec une syntaxe qui n’est pas sans rappeler celle des symboles.
def introduce(first_name, last_name, age, location: 'Mickeyville', reverse: false)
return "#{last_name} #{first_name} a #{age} ans et habite à #{location}." if reverse
"#{first_name} #{last_name} a #{age} ans et habite à #{location}."
end
introduce('Mickey', 'Mouse', 92, reverse: true)
introduce('Donald', 'Duck', 86, reverse: true, location: 'Donaldville')
L’ordre des paramètres nommés n’a pas d’importance. Ainsi, nous pouvons donner l’argument pour reverse
avant celui pour location
(comme nous l’avons fait dans notre dernier exemple). Notons de plus que nous ne sommes pas obligés de donner une valeur par défaut à notre argument. Nous pouvons transformer notre méthode pour n’utiliser que des paramètres nommés.
def introduce(first_name:, last_name:, age:, location: 'Mickeyville', reverse: true)
return "#{last_name} #{first_name} a #{age} ans et habite à #{location}." if reverse
"#{first_name} #{last_name} a #{age} ans et habite à #{location}."
end
introduce(first_name: 'Donald', last_name: 'Duck', location: 'Donaldville', age: 86)
Nous pouvons alors appeler introduce
sans nous soucier de l’ordre des arguments que l’on donne. Cependant, puisque cet ordre n’a aucune importance, il est obligatoire de préciser le nom du paramètre lors de l’appel de la méthode.
introduce('Donald', 'Duck', 86, true, 'Donaldville')
# => ArgumentError
Nous pourrions nous attendre à ce que Ruby décide d’utiliser l’ordre des arguments pour décider des paramètres dans lesquels ils sont récupérés, mais ce n’est pas le cas, nous obtenons une erreur ArgumentError
. Les paramètres nommés sont donc un peu plus lourd à utiliser et nous verrons comment les utiliser à bon escient.
Mélanger les deux types de paramètres
Notre premier exemple de méthode avec des paramètres nommés avait également des paramètres positionnels. Si c’est autorisé par le langage, il y a une contrainte à respecter : les paramètres positionnels doivent apparaître avant les paramètres nommés. La même contrainte est à respecter pour les arguments lors de l’appel de la méthode.
Si une définition de méthode ne respecte pas cela, nous obtiendrons une erreur de syntaxe SyntaxError
, et si c’est lors d’un appel de méthode que nous ne respectons pas la contrainte, nous aurons une erreur ArgumentError
.
def f(x: 2, y)
x + y
end
# => SyntaxError
def g(y, x: 2)
x + y
end
g(x: 3, 2) # => ArgumentError
Cette contrainte n’est pas compliquée à respecter et se comprend plutôt bien : d’abord on regarde les choses qui sont liées à une position, et ensuite on regarde les choses qui peuvent apparaître dans n’importe quel ordre.
Les paramètres nommés
Les paramètres nommés nous ont permis de résoudre notre problème précédent. Mais quand faut-il les utiliser ? Était-ce la bonne solution pour notre problème ? Pour répondre à cette question, il faut se rendre compte que notre but, en écrivant du code, est d’obtenir un code clair et lisible. Ce constat nous donne plusieurs cas d’usage des paramètres nommés.
Cas d’usage
Parfait pour des options
Le premier d’entre eux correspond à la manière dont nous l’utilisons pour reverse
. Dans notre cas, notre méthode a deux modes de fonctionnement en fonction de la valeur de reverse
. C’est un usage très répandu, car, sans le nom du paramètre, il peut être compliqué de savoir à quoi correspond l’argument.
introduce('Mickey', 'Mouse', 92, true)
introduce('Mickey', 'Mouse', 92, reverse: true)
Dans le premier cas, l’appel de méthode ne permet pas vraiment de savoir à quoi correspond le dernier argument. Dans le second cas, c’est déjà beaucoup plus clair.
En fait, ce guide de bonnes pratiques recommande d’utiliser des paramètres nommés lorsqu’un booléen est attendu !
Dans la librairie standard, nous avons quelques exemples de méthode avec de tels paramètres. La méthode round
, par exemple, permet d’arrondir les flottants. Elle a un paramètre nommé, half
, qui indique comment arrondir si la partie décimale du flottant est 0.5
. Avec :up
on arrondit au supérieur (c’est la valeur par défaut), avec :down
à l’inférieur, et avec :even
on arrondit à l’entier pair.
2.5.round # => 3
2.5.round(half: :up) # => 3
2.5.round(half: :down) # => 2
2.5.round(half: :even) # => 2
3.5.round(half: :even) # => 4
Et pour ne pas se soucier de l’ordre
L’utilisation des paramètres nommés pour des options est assez fréquent. Tellement qu’il est le plus souvent croisé avec des valeurs par défaut. Pourtant, comme nous l’avons vu, les valeurs par défaut ne sont pas obligatoires avec des paramètres nommés. Nous pouvons juste les utiliser pour ne pas nous soucier de l’ordre des paramètres. Ainsi, nous avons précédemment écrit ce code.
def introduce(first_name:, last_name:, age:, location: 'Mickeyville', reverse: true)
return "#{last_name} #{first_name} a #{age} ans et habite à #{location}." if reverse
"#{first_name} #{last_name} a #{age} ans et habite à #{location}."
end
introduce(first_name: 'Donald', last_name: 'Duck', location: 'Donaldville', age: 86)
Ici, pas besoin de connaître l’ordre des arguments dans la définition de la méthode. Bien sûr,
De la clarté !
Comme nous ne cessons de le répéter, nous devons viser un code clair. Nous voulons un code facile à lire et facile à écrire et les paramètres nommés sont un outil supplémentaire pour cela. En particulier, cela signifie qu’ils ne sont pas utiles tout le temps. Par exemple, personne n’aurait l’idée d’avoir un paramètre nommé pour une méthode comme puts
! Si on se base toujours sur l’exemple de notre méthode introduce
, un paramètre nommé pour reverse
est raisonnable et rend effectivement notre code plus clair. Cependant, des paramètres positionnels semblent corrects pour les paramètres restants. Nous pouvons donc écrire cette méthode introduce
.
def introduce(first_name, last_name, age, location = 'Mickeyville', reverse: false)
return "#{last_name} #{first_name} a #{age} ans et habite à #{location}." if reverse
"#{first_name} #{last_name} a #{age} ans et habite à #{location}."
end
Ici, nous utilisons les valeurs par défaut des paramètres positionnels pour location
afin de le traiter comme les autres paramètres qui ne se rapprochent pas d’une option. Ainsi, seul le paramètre reverse
est nommé et nous pouvons faire ces différents appels.
introduce('Mickey', 'Mouse', 92, reverse: true)
introduce('Donald', 'Duck', 86, 'Donaldville')
introduce('Donald', 'Duck', 86, 'Donaldville', reverse: true)
Certains pourraient trouver que l’ordre n’est pas clair pour les paramètres positionnels ici. Nous entrons là dans le domaine du subjectif et chacun décidera alors de la nature des paramètres. Cependant, il semble raisonnable de ne pas avoir first_name
en paramètre positionnel et last_name
en paramètre nommé !
La méthode lines
illustre bien ce point. Elle permet de découper une chaîne de caractères en un tableau de sous-chaînes. Les découpes se font à chaque fois qu’un séparateur apparaît dans la chaîne, ce séparateur étant un paramètre positionnel qui vaut \n
par défaut. La méthode a également un paramètre nommé, chomp
qui vaut false
par défaut et qui indique s’il faut supprimer les séparateurs. Nous pouvons alors faire les appels suivants, tirés de la documentation de la méthode.
"hello\nworld\n".lines #=> ["hello\n", "world\n"]
"hello world".lines(' ') #=> ["hello ", " ", "world"]
"hello\nworld\n".lines(chomp: true) #=> ["hello", "world"]
"hello word".lines(' ', chomp: true) #=> ["hello", "world"]
Le premier argument est plutôt clair puisque l’on sait ce que la méthode fait. Le second argument, quant à lui, mériterait quelques éclaircissements s’il n’était pas nommé. C’était donc une bonne idée d’avoir chomp
en paramètre nommé. Nous remarquons d’ailleurs qu’il s’agit là du premier cas d’usage dont nous avons parlé ; chomp
est une option de la méthode. Mais même là, nous pourrions trouver que str.lines(' ')
n’est pas clair et vouloir faire du séparateur un paramètre nommé… Et ce ne serait pas forcément absurde !
Retour sur les valeurs par défaut
Pour finir ce chapitre, nous allons voir quelques subtilités des valeurs par défaut
Valeurs par défaut
Les valeurs par défaut sont assez simples lorsque nous utilisons des paramètres nommés. En effet, vu que ces paramètres n’ont pas d’ordre, on peut donner une valeur par défaut à n’importe quel paramètre nommé.
def introduce(first_name, last_name, age:, reverse: false, location:, universe: 'Disney')
if reverse
"#{last_name} #{first_name} [#{universe}] a #{age} ans et habite à #{location}."
else
"#{first_name} #{last_name} [#{universe}] a #{age} ans et habite à #{location}."
end
end
introduce('Mickey', 'Mouse', age: 92, location: 'Mickeyville', reverse: false)
introduce('Bruce', 'Wayne', age: 43, location: 'Gotham City', universe: 'DC')
Ici, nous pouvons avoir deux paramètres nommés avec un argument par défaut, reverse
et universe
. Puisque les paramètres sont nommés, Ruby n’a aucun mal à savoir quel argument correspond à quel paramètre lorsque l’on appelle la méthode. Avec des paramètres positionnels, ce n’est pas le cas.
def introduce(first_name, last_name, age, universe = 'Disney', location, reverse = false)
if reverse
"#{last_name} #{first_name} [#{universe}] a #{age} ans et habite à #{location}."
else
"#{first_name} #{last_name} [#{universe}] a #{age} ans et habite à #{location}."
end
end
Ici, Ruby nous donne une erreur SyntaxError
dès la définition de la méthode. En fait, les paramètres positionnels avec une valeur par défaut doivent se suivre et former un seul bloc. La définition suivante, elle, est correcte.
def introduce(first_name, last_name, age, universe = 'Disney', reverse = false, location)
if reverse
"#{last_name} #{first_name} [#{universe}] a #{age} ans et habite à #{location}."
else
"#{first_name} #{last_name} [#{universe}] a #{age} ans et habite à #{location}."
end
end
La méthode attend un nombre i
de paramètres positionnels obligatoires, puis j
paramètres positionnels facultatifs, et ensuite un nombre j
de paramètres positionnels obligatoires (dans notre exemple, i
vaut 3
, j
vaut 2
et k
vaut 1
). L’appel de la méthode se passe alors de la façon suivante.
- Les
i
premiers arguments seront récupérés dans lesi
premiers paramètres positionnels (qui sont obligatoires). - Les
j
derniers arguments seront récupérés dans lesj
derniers paramètres positionnels (qui sont obligatoires). - Les arguments restants, s’il y en a, seront récupérés par les paramètres facultatifs, dans l’ordre.
Ainsi, Ruby peut dans tous les cas déterminer à quel argument correspond un paramètre. Si les paramètres avec une valeur par défaut ne se suivaient pas, ce serait possible, mais un peu plus embêtant. Regardons les résultats des différents appels suivants.
introduce('Mickey', 'Mouse', 92, 'Mickeyville')
introduce('Bruce', 'Wayne', 43, 'DC', 'Gotham City')
introduce('Bruce', 'Wayne', 43, 'DC', true, 'Gotham City')
Nous remarquons que nous ne pouvons pas donner de valeur pour le paramètre reverse
sans en donner pour le paramètre universe
. C’est une des raisons qui explique que l’on préfère utiliser des paramètres nommés pour des paramètres correspondant à des options : pour modifier juste option2
, nous ne voulons pas avoir à donner de valeur pour option1
.
Et nous connaissons maintenant les règles d’utilisation des valeurs par défaut. Bien sûr, ce n’est pas parce qu’il est possible de faire tout ceci qu’il faut à tout prix en abuser. Nous pourrions écrire une méthode avec des paramètres positionnels obligatoires, des paramètres positionnels facultatifs, des paramètres nommés obligatoires et des paramètres nommés facultatifs, mais il faudrait une très bonne excuse pour cela !
Finalement, nous écrirons généralement des méthodes avec un nombre raisonnable d’arguments (nous devrions éviter d’avoir plus de trois ou quatre paramètres) et en utilisant les paramètres comme nous l’avons vu précédemment.
La plupart des guides de bonnes pratiques conseillent également de placer les paramètres optionnels à la fin de la liste des paramètres et certains conseillent même de préférer les paramètres nommés aux paramètres positionnels pour les paramètres optionnels.
Référence à d’autres paramètres
Parmi les autres choses possibles avec les paramètres de Ruby, il y a la possibilité d’utiliser un paramètre pour définir une valeur par défaut d’un autre paramètre. En voici un exemple.
def f(x, y = x + 1)
"x : #{x} et y : #{y}"
end
f(2, 3) # => x : 2 et y : 3
f(2) # => x : 2 et y : 4
Ici, la valeur par défaut du paramètre y
dépend de la valeur donnée au paramètre x
. Cela semble inutile, mais voici un exemple d’utilisation. Écrivons une méthode qui doit créer un compte avec un nom, un prénom et un pseudo. Par défaut, le pseudo est nom.prénom
.
def create_account(first_name, last_name, username: "{first_name}.#{last_name}")
puts "Le compte #{username} de #{first_name} #{last_name} a été créé."
end
create_account('Mickey', 'Mouse')
create_account('Donald', 'Duck', username: "onc' Donald")
create_account('Antonhy', 'Stark', username: 'Tony@Stark')
Dans ce dernier exemple, nous utilisons les paramètres first_name
et last_name
pour définir la valeur par défaut de username
. Remarquons de plus que nous l’utilisons cette fois avec un paramètre nommé.
Ce procédé a une seule petite contrainte ; lorsque nous l’utilisons pour définir un paramètre param
, nous ne pouvons utiliser que les paramètres qui apparaissent avant param
.
Les codes suivants nous donneront alors une erreur.
def f(x = y + 1, y)
[x, y]
end
f(1, 2) # => [1, 2]
f(1) # => Ne donne pas y + 1 mais une NameError.
Ici, nous voyons de plus que l’appel de méthode est correct lorsque nous fournissons des arguments pour tous les paramètres. Ce n’est que dans le deuxième appel que nous obtenons une erreur. Essayons de comprendre ce qu’il peut se passer.
- On voit
f(1)
, on comprend qu’il faut appeler la méthode. - La méthode a deux paramètres et l’appel est fait avec un argument, la valeur de cet argument est donc pour le deuxième paramètre (celui qui n’a pas de valeur par défaut).
- On essaye de définir le premier paramètre. Pour cela, on crée une variable
x
et on lui donne la valeury
. Maisy
n’existe pas, on obtient l’erreur undefined local variable or method `y' for main:Object.
En fait, lorsqu’on rentre dans une méthode, de nouvelles variables sont définies pour chaque paramètre, et elles sont définies dans l’ordre des paramètres, donc pour définir un paramètre param
, on ne peut utiliser que des paramètres présents avant param
.
Là encore, ce n’est pas quelque chose qui est très souvent utilisé, même s’il peut être utilisé dans certaines situations comme l’a montré l’exemple create_account
.
Finalement, même si certaines subtilités sont présentes, notamment au niveau des valeurs par défaut, les deux types de paramètres ne sont pas vraiment compliqués à comprendre, ni à utiliser. Retenons également que le plus important est d’avoir un code clair et lisible et que l’abus de syntaxes compliquées est à l’opposé de ce but.