Listes d'arguments

Après avoir vu les deux types de paramètres des méthodes de Ruby, nous allons nous intéresser à une mécanique permettant de récupérer plusieurs arguments dans un seul paramètre qui correspondra alors soit à une liste, soit à une table de hachage.

L'opérateur de splat

Pour commencer, nous allons parler d’un opérateur appelé opérateur de splat. Cet opérateur est principalement utilisé pour construire et déconstruire des tableaux en récupérant plusieurs éléments de la manière suivante.

# On déconstruit ary en récupérant les deux premiers
# éléments, le dernier, et ce qui reste au milieu.
ary = [1, 2, 3, 4, 5]
a, b, *middle, c = ary
print middle # => [3, 4]

# On construit un tableau en utilisant les
# éléments de deux tableaux.
ary2 = [1, 2, *middle, 5, *[6, 7]]
print ary2 # => [1, 2, 3, 4, 5, 6, 7]

Avec les paramètres positionnels

L’opérateur de splat * est utilisé de manière similaire à ce que nous venons de faire avec les tableaux. D’un côté, il permet de transformer un tableau en une liste d’arguments (ça correspond à déconstruire le tableau).

def f(x, y, z, t, u, v)
  [x, y, z, t, u, v]
end

ary = [1, 2, 3, 4, 5, 6]
f(*ary) # Est équivalent à f(1, 2, 3, 4, 5, 6)
f(ary) # Donne une erreur, un seul argument est donné à f.

ary = [3, 4]
f(1, 2, *ary, 5, 6) # => [1, 2, 3, 4, 5, 6]

ary1 = [2, 3]
ary2 = [5]
f(1, *ary1, 4, *ary2, 6) # => [1, 2, 3, 4, 5, 6]

Finalement, ici, il sert à « aplatir » le tableau dans les arguments. Nous pouvons d’ailleurs en avoir plusieurs comme nous l’avons fait dans le dernier exemple.

Et de l’autre côté, l’opérateur de splat permet de transformer une liste d’arguments en tableau. C’est ce qu’il fait lorsqu’il est dans une définition de méthode.

def f(*ary)
  ary
end

f             # => []
f(1, 2)       # => [1, 2]
f(1, 2, 3, 4) # => [1, 2, 3, 4]
f([1, 2])     # => [[1, 2]]

On obtient ainsi une méthode qui prend un nombre quelconque d’arguments qu’elle récupère dans un tableau. C’est par exemple le cas de la méthode concat des chaînes de caractères. Elle permet de concaténer toutes les chaînes passées en paramètre à la chaîne de départ.

'ab'.concat('cd', 'ef', 'gh') # => 'abcdefgh'

L’opérateur de splat est très puissant. Il faut cependant parfois se demander si la méthode attend un tableau ou une liste d’arguments (on obtient une erreur en donnant un tableau à concat et dans notre dernier exemple avec f, nous obtenons [[1, 2]] en donnant à f un tableau), mais ce n’est pas très gênant. Au contraire, cela nous donne plutôt de la flexibilité dans l’écriture du code.

Notons de plus que nous pouvons accepter d’autres arguments positionnels.

def f(x, y, *ary, z, t)
  ary
end

f(1, 2, 3, 4, 5, 6, 7) # => [3, 4, 5]
f(1, 2, 3, 4)          # => []

Les limites

En revanche, nous ne pouvons avoir qu’un seul paramètre avec un splat. Et c’est plutôt logique, dans le code qui suit, nous n’avons aucune manière de savoir à quels arguments doivent correspondre ary1 et ary2 (et nous avons ce problème dès qu’il y a plus d’un paramètre avec splat).

def f(x, *ary1, y, *ary2, z)
  ary1
end

f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Finalement, voyons comment se comporte l’opérateur de splat en présence d’un paramètre avec une valeur par défaut. En fait, les paramètres ayant une valeur par défaut doivent être placés avant l’éventuel paramètre avec le splat.

# Donne une erreur.
def f(x, y, *ary, z = 'z')
  print "x : #{x}, y : #{y}, ary : #{ary} et z : #{z}."
end

# Est OK.
def f(x, y = 'y', **ary, z)
  print "x : #{x}, y : #{y}, ary : #{ary} et z : #{z}."
end

À part ça, le comportement est normal. On remplit d’abord les paramètres obligatoires, puis les paramètres facultatifs, et s’il reste des paramètres, ils iront dans le paramètre splat.

f(0, 4)          # x : 0, y : y, ary : [] et z : 4.
f(0, 1, 4)       # x : 0, y : 1, ary : [] et z : 4.
f(0, 1, 2, 3, 4) # => x : 0, y : 1, ary : [2, 3] et z : 4.

Bien sûr, ce n’est pas quelque chose que nous croiserons tous les jours. De manière générale, il vaut mieux avoir une méthode qui ne prend pas trop de paramètres. Nous voulons des méthodes qui font une action et la font bien (conformément à tout ce qui est principe de responsabilité unique, KISS et autres). Ainsi, une méthode qui prend trop d’arguments n’est pas une bonne idée !

L'opérateur de double splat

Avec les paramètres nommés

Il y a également un opérateur de splat utilisé avec les paramètres nommés. Il s’agit cette fois du double splat ** et lui permet de faire le passage entre table de hachage à paramètres nommés. En dehors de ceci, il fonctionne comme l’opérateur de splat.

def compute_price(price, tva: 0.2, offer: 0)
  price * (1 + tva) - offer
end

compute_price(100, offer: 10) # => 110.0

hash = {tva: 0.18, offer: 6}
compute_price(100, **hash)  # => 112.0
# Équivalent à compute_price(100, tva: 0.18, offer: 6)

compute_price(100, **{tva: 0.3}) # => 130.0

Dans « l’autre sens », nous pouvons récupérer une table de hachage dans une méthode en l’appelant avec des arguments nommés.

def build_computer(**components)
  puts 'On construit un ordinateur avec les composants suivants :'
  components.each do |c, v|
    puts "- #{c} : #{v}."
  end
end

build_computer(
  "carte mère": 'celle-là',
  écran: 'lui',
  processeur: 'un bon processeur',
  "carte graphique": 'une géniale'
)
Point de syntaxe

La syntaxe :"str" permet de déclarer un symbole équivalent à :str. Ainsi, :"écran déclare le symbole :écran. Cette syntaxe permet notamment de déclarer des symboles contenant des espaces. Ici, avec "carte mère": value, nous avons la clé :carte mère et la valeur value.

Ici, nous sommes intéressés par une table de hachage (que nous voulons parcourir), nous préférons donc une table de hachage à des paramètres nommés. Cela nous permet de plus de faire facilement des ordinateurs plus ou moins complets ; nous pouvons facilement rajouter une carte réseau ou un disque SSD, là où avec des paramètres nommés il nous faudrait rajouter un paramètre pour la carte réseau, un autre pour le SSD (et avec des valeurs par défaut pour beaucoup d’entre eux).

Nous ne pouvons avoir qu’un seul paramètre avec un double splat, mais contrairement au splat, il est obligatoirement à la fin des paramètres nommés.

def f(arg1: 1, arg2: 2, **hash)
  hash
end

f(other: 3, key: 4) # => {other: 3, key: 4}

# Le code qui suit donne une erreur
def g(**hash, arg1: 1, arg2: 2)
  hash
end
Paramètres nommés et table de hachage, la même chose ?

Nous constatons facilement le lien fort entre les paramètres nommés et les tables de hachage. En fait, avant la version 2.7 de Ruby, il y avait des conversions implicites dans tous les sens entre les tables de hachage et les paramètres nommés. Ce code était par exemple valide.

def f(arg:)
  arg
end

hash = {arg: 2}
f(hash) 

Depuis la version 2.7, ce code nous donne un avertissement transformé en erreur depuis la version 3 de Ruby. La méthode attend un argument nommé et nous lui donnons un argument positionnel (qui se trouve être une table de hachage), nous obtenons une erreur ArgumentError (wrong number of arguments (given 1, expected 0; required keyword: arg)). Pour le corriger, nous devons explicitement donner à f des paramètres nommés.

f(**hash) # => 2

En fait, une conversion implicite a été conservée, parce qu’elle était totalement claire, sans ambiguïté. Il s’agit du cas où une méthode a un paramètre positionnel et que nous lui donnons des arguments nommés. Ruby va alors automatiquement convertir ces arguments nommés en table de hachage.

def f(hash)
  hash
end

f(arg1: 1, arg2: 2) # => {arg1: 1, arg2: 2}

def g(x, hash)
  hash
end

g(1, arg1: 1, arg2: 2) # => {arg1: 1, arg2: 2}

# L'appel qui suit donne une erreur.
g(arg1: 1, 1)

def h(x, hash, key: 1)
  hash
end

# Ne fonctionne pas si la méthode a des paramètres nommés
h(1, arg1: 1, key: 1)

Cette conversion ne fonctionne que s’il ne peut vraiment pas y avoir d’ambiguïté.

  • Comme h a des paramètres nommés, on obtient une erreur.
  • Le second appel de g donne une erreur, car les arguments nommés doivent être donnés après les arguments positionnels.

En particulier, ces deux informations signifient que pour que cette conversion fonctionne, les arguments nommés que l’on veut « convertir » doivent être en dernier et seront donc forcément récupérés dans le dernier argument de la méthode (argument qui est forcément positionnel).


Finalement, une méthode a d’abord ses paramètres positionnels, parmi lesquels il peut y avoir un paramètre avec un splat (généralement le dernier, même si ce n’est pas obligatoire), puis ses paramètres nommés et ensuite potentiellement un paramètre avec un double splat.

def f(x = 'x', *ary, y, i: 'i', j: 'j', **hash)
  puts "ary : #{ary}."
  puts "x vaut #{x} et y vaut #{y}."
  puts "i: #{i} et j: #{j}."
  puts "hash : #{hash}."
end

f(1)
f(0, 1)
f(1, a: 2, b: 3)
f(0, 1, 2, k: -1, i: 0, l: -2)

Histoire des paramètres nommés

L’avant Ruby 2

Avant la version 2 de Ruby, il n’y avait pas de paramètre nommé. Mais l’envie de tels paramètres existait déjà, et ils étaient souvent simulés à l’aide de table de hachage. L’idée est simple : la méthode prend en paramètre une table de hachage dont les clés sont les noms que nous aurions voulu pour les paramètres. Une version simple de introduce pourrait alors s’écrire de la manière suivante.

def introduce(kwargs)
  first_name = kwargs[:first_name]
  last_name = kwargs[:last_name]
  age = kwargs[:age]
  "#{first_name} #{last_name} a #{age} ans."
end

introduce({first_name: 'Mickey', age: 92, last_name: 'Mouse'})

Notons que nous pouvons utiliser la méthode fetch_values pour récupérer plusieurs clés d’une table de hachage d’un coup. De plus, il est également possible de simuler des valeurs par défaut à l’aide de la méthode merge. Elle s’applique à une table de hachage h et renvoie la fusion de cette table avec les arguments passés à la méthode (et si une clé est présente dans h et dans une table en argument, c’est la valeur de cette dernière table qui est conservée).

def introduce(kwargs)
  hash = {reverse: false, location: 'Mickeyville'}.merge(kwargs)
  first_name, last_name, age, location, reverse = hash.fetch_values(
    :first_name,
    :last_name,
    :age,
    :location,
    :reverse
  )
  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: 'Mickey', age: 92, last_name: 'Mouse', reverse: true})
introduce({first_name: 'Donald', last_name: 'Duck', location: 'Donaldville', age: 86})

Introduction des paramètres nommés

Le code est correct, mais la syntaxe est plus lourde que celle que nous obtenons avec des paramètres nommés. Les paramètres nommés arrivent donc… À point nommé. De plus, ils donnent des erreurs beaucoup plus claires. Lorsqu’il manque un paramètre, nous obtenons une ArgumentError nous indiquant le paramètre manquant dans le cas des paramètres nommés. Avec une table de hachage, ce serait une KeyError dans le cas où nous utiliserions fetch_values et avec la syntaxe hash[:key] nous n’obtenons même pas d’erreur, mais nil si :key n’est pas une clé de hash !

def introduce(kwargs)
  first_name = kwargs[:first_name]
  last_name = kwargs[:last_name]
  age = kwargs[:age]
  "#{first_name} #{last_name} a #{age} ans."
end

introduce({}) #=> '  a  ans.'

De plus, donner une table de hachage avec des clés supplémentaires ne cause aucune erreur. Ce n’est généralement pas très gênant, mais ce serait mieux ! Imaginons par exemple que je crois avoir programmé introduce avec reverse mais que ce n’est pas le cas. Mes appels avec la clé reverse dans la table de hachage ne me permettent pas de savoir que je ne l’ai pas fait.

Toujours est-il que les paramètres nommés arrivent avec Ruby 2 et permettent d’écrire ce genre de code plus facilement.

Le passage de témoin

Avec ces paramètres nommés, Ruby a l’idée de permettre de donner une table de hachage en argument là où des paramètres nommés sont attendus (les valeurs sont alors automatiquement récupérées dans la table de hachage). Ça permettait notamment aux codes comme celui qui suit de fonctionner et de ne pas avoir à déconstruire la table de hachage pour passer chaque argument à la méthode.

def introduce(first_name:, last_name:, age:)
  "#{first_name} #{last_name} a #{age} ans."
end

hash = {first_name: 'Mickey', age: 92, last_name: 'Mouse'}
introduce(hash)

Ainsi, une méthode pouvait être réécrite pour utiliser les paramètres nommés à la place d’une table de hachage sans pour autant changer tous les appels à la méthode, nous n’avions pas besoin de double splat pour transformer la table de hachage passée en argument.

Cela a permis d’opérer une migration en douceur. Comme nous l’avons vu précédemment, ce code ne fonctionne plus depuis la version 3 de Ruby (et donne un avertissement en Ruby 2.7).

Connaître l’histoire des paramètres nommés permet de savoir pourquoi il peut exister une certaine confusion avec les tables de hachage, et pourquoi certaines conversions étaient autorisées. En clair, cela permet de comprendre certains choix de l’équipe de développement de Ruby.


Les opérateurs de splat et de double splat sont très utiles et très puissants… Mais pas forcément adaptés à toutes les situations. Comme nous l’avons vu, les paramètres nommés offrent cependant de meilleurs messages d’erreurs que ceux obtenus avec un double splat ; ce dernier est néanmoins utile dans certains cas (dans l’exemple de construction d’un ordinateur, n’importe quelle clé est acceptée).