Les bases du langage de programmation Ruby en 20 minutes

Ce court tutoriel ne devrait pas prendre plus de vingt minutes de votre temps. Il part du principe que vous avez déjà installé Ruby ; si ce n’est pas le cas, il vous est chaleureusement conseillé de le faire avant de poursuivre votre lecture.

Interactive Ruby

Une première approche de Ruby, la plus simple, consiste à utiliser IRB (Interactive Ruby) dans un terminal :

  • si vous utilisez macOS, ouvrez un Terminal et tapez irb, puis appuyez sur [Entrée] ;
  • si vous êtes sous Linux, ouvrez une console et tapez irb, puis appuyez sur [Entrée] ;
  • si enfin, vous utilisez Windows, lancez Interactive Ruby qui se trouve dans la section Ruby du menu Démarrer > Applications.

IRB permet d’écrire et d’interpréter du Ruby à la volée, sans avoir à enregistrer un fichier et l’exécuter ensuite.

irb(main):001:0>

Voilà, IRB est lancé. Et maintenant ?

Tapez ceci : "Hello World" et appuyez sur [Entrée] pour valider (ce que vous devrez faire après chaque nouvelle ligne).

irb(main):001:0> "Hello World"
=> "Hello World"

Ruby… au doigt et à l’œil

Que vient-il de se passer au juste ? Venons-nous d’écrire le plus court programme Hello World jamais vu ? Pas tout à fait… mais presque. La seconde ligne, celle débutant par le signe =>, est la façon dont IRB nous informe du résultat de la dernière expression évaluée. Si nous voulons véritablement écrire « Hello World », il en faut un tout petit peu plus :

irb(main):002:0> puts "Hello World"
Hello World
=> nil

puts est la commande de base pour écrire quelque chose avec Ruby. Et qu’est-ce que nil que nous présente IRB ? Le résultat de notre expression puts "Hello World", bien sûr ! En fait, puts retourne toujours nil, qui est, pour Ruby et bientôt, pour vous, l’absence totale de valeur.

Premiers pas en calcul

Il est très facile d’utiliser IRB comme une vulgaire calculatrice :

irb(main):003:0> 3+2
=> 5

Trois plus deux. Assez facile, j’en conviens. Et quid de trois fois deux ? Notez ici que vous pouvez, si vous le voulez, appuyer sur la touche [Flèche haut] pour rappeler la dernière ligne tapée dans IRB, et avec les flèches directionnelles et [backspace], remplacer le signe + par *:

irb(main):004:0> 3*2
=> 6

Maintenant, essayons trois au carré :

irb(main):005:0> 3**2
=> 9

Dans la syntaxe Ruby, ** est là pour signifier « à la puissance… » ; et si vous souhaitez aller dans l’autre sens, à savoir obtenir la racine carré d’un nombre, vous écrirez ceci :

irb(main):006:0> Math.sqrt(9)
=> 3.0

Un peu moins trivial. Vous avez vraisembablement deviné que cela demandait la racine carré (Ndt : square root en anglais). Mais qu’est-ce que ce Math ?

Les modules, ou comment regrouper le code par thématiques

Math est ce qu’on appelle en Ruby, un module. Il vous est fournit d’office (il fait partie de la bibliothèque standard, qui regroupe les modules d’intérêt général) et s’occupe, comme son nom l’indique, des mathématiques. Les modules ont deux grands rôles à jouer au sein de Ruby. L’exemple précédent illustre le premier de ces rôles : regrouper des méthodes (souvent désignées sous le vocable de « fonctions » dans d’autres langages). Par exemple, le module Math contient aussi, et entre autres, les méthodes sin et tan, deux fonctions trigonométriques biens connues.

Après Math venait, dans notre code source, un point « . » collé entre Math et la méthode sqrt. Quel est son rôle ? Le point permet d’identifier sans ambiguïté qui doit recevoir un message. La question se pose donc de savoir ce qu’est un « message » en Ruby. Dans notre, exemple, le message est tout simplement l’action désirée, à savoir sqrt(9): « récupérer la racine carrée du chiffre 9. » Pour Ruby, ce message consiste à appeler la méthode sqrt, avec le paramètre 9, et à retourner le résultat.

Comme vous le voyez dans IRB, le résultat de cet appel de méthode est la valeur numérique 3.0. Vous remarquerez ici que ce n’est pas simplement l’entier naturel 3. Parceque bien souvent, le résultat d’une racine carrée n’est pas un entier, la méthode retourne par défaut un nombre réel (du point de vue de l’ordinateur, un nombre à virgule flottante).

Si nous souhaitons conserver « quelque part » le résultat de cette opération mathématique, il suffit de l’assigner à une variable.

irb(main):007:0> a = 3 ** 2
=> 9
irb(main):008:0> b = 4 ** 2
=> 16
irb(main):009:0> Math.sqrt(a+b)
=> 5.0

Bien, notre calculatrice est déjà un bon début, mais le sujet principal de ce tutoriel est le programme Hello World.

Admettons, pour la bonne continuité de ce tutoriel, que nous voulons tout à coup répéter et répéter encore « Hello World » … mais sans trop nous fatiguer. Il va falloir définir une méthode.

irb(main):010:0> def h
irb(main):011:1> puts "Hello World!"
irb(main):012:1> end
=> nil

Le mot-clé def a ici pour rôle de commencer la définition d’une méthode. Il dit tout simplement à Ruby que nous [attention !] commençons à écrire [grands signes vers Ruby] une nouvelle méthode, laquelle s’appelle h. La ligne suivante constitue le corps de notre méthode ; en l’occurence, il s’agit de la ligne que nous avons écrite auparavant, puts "Hello World". Enfin, la dernière ligne, end, indique à Ruby que la définition de la méthode s’arrête . Après avoir appuyé sur [Entrée], Ruby répond… nil, ce qui est sa façon à lui de dire qu’il a bien compris et enregistré notre nouvelle méthode.

Courte et morne vie d’une méthode…

Bien, essayons de faire fonctionner cette méthode deux ou trois fois, pour voir :

irb(main):013:0> h
Hello World!
=> nil
irb(main):014:0> h()
Hello World!
=> nil

Rien de très compliqué. Appeler une méthode en Ruby, c’est simplement dire son nom, au minimum. Si la méthode n’attend pas de paramètres pour préciser ce qu’elle doit faire, le nom suffira. Et d’ailleurs, vous pouvez bien ajouter des parenthèses vides à une méthode qui n’attend pas de paramètres : elles seront simplement ignorés.

Que faire si nous souhaitons dire bonjour à une personne en particulier, et pas au monde entier ? Et bien, il suffit justement de demander à ce que le nom de la personne concernée soit mentionné en tant que paramètre de la méthode h:

irb(main):015:0> def h(name)
irb(main):016:1> puts "Hello #{name}!"
irb(main):017:1> end
=> nil
irb(main):018:0> h("Matz")
Hello Matz!
=> nil

Ça fonctionne… mais pourquoi ? et comment ?

Les chaînes des caractères

Le truc le plus étrange dans ce nouvel exemple est sûrement ce #{name}. Il s’agit en fait du moyen le plus simple dont dispose Ruby pour insérer quelque chose au sein d’une chaîne de caractère. Le mot entre crochets, name, est à terme transformé en une chaîne de caractère (sauf s’il en était déjà une, bien sûr) et incorporé à la chaîne qui l’englobe ("Hello !"). Et comme, au départ, vous manipulez une variable (name), vous pouvez lui passer des messages avec des méthodes… Par exemple, lui demander de toujours débuter par une majuscule avec capitalize:

irb(main):019:0> def h(name = "World")
irb(main):020:1> puts "Hello #{name.capitalize}!"
irb(main):021:1> end
=> nil
irb(main):022:0> h "chris"
Hello Chris!
=> nil
irb(main):023:0> h
Hello World!
=> nil

D’autres petits ajouts ont eu lieu dans ce dernier bout de code. Tout d’abord, nous n’appelons plus la méthode avec des parenthèses autour du paramètre. Elles sont en effet optionnelles, et vous pouvez vous en passer si ce que vous écrivez est trivial, comme c’est le cas ici. Ensuite, nous avons défini une valeur par défaut pour le paramètre name. Si la méthode est appelée sans que lui soit fourni un nom en particulier, alors le nom prendra la valeur "World".

Un brin de politesse

L’accueil donné actuellement par notre méthode est un peu frustre… il ne serait pas de trop d’avoir une formule plus respectueuse à l’encontre de l’usager. Pour ce faire, nous utiliserons un objet. Et nous allons même dans la foulée créer une classe, “Greeter”.

irb(main):024:0> class Greeter
irb(main):025:1>   def initialize(name = "World")
irb(main):026:2>     @name = name
irb(main):027:2>   end
irb(main):028:1>   def say_hi
irb(main):029:2>     puts "Hi #{@name}!"
irb(main):030:2>   end
irb(main):031:1>   def say_bye
irb(main):032:2>     puts "Bye #{@name}, come back soon."
irb(main):033:2>   end
irb(main):034:1> end
=> nil

Évidemment, la nouveauté la plus importante ici est le mot-clé class. Son rôle est de définir une nouvelle classe, en l’occurence Greeter, laquelle classe contient plusieurs méthodes. Vous avez également repéré @name. Il s’agit d’une « variable d’instance », qui sera disponible pour toutes les méthodes de la classe Greeter. Elle est d’ailleurs utilisée par les méthodes say_hi et say_bye.

Maintenant, il s’agit de donner vie à notre classe. Donc de créer un objet…

Maintenant, créons un objet de la classe Greeter et animons-le :

irb(main):035:0> g = Greeter.new("Pat")
=> #<Greeter:0x16cac @name="Pat">
irb(main):036:0> g.say_hi
Hi Pat!
=> nil
irb(main):037:0> g.say_bye
Bye Pat, come back soon.
=> nil

Une fois l’objet g créé, il se souvient que le nom qui lui est lié est Pat, comme indiqué à sa création. Il serait d’ailleurs intéressant de pouvoir récupérer directement ce nom. Essayons :

irb(main):038:0> g.@name
SyntaxError: compile error
(irb):52: syntax error
        from (irb):52

Whoa, ça n’a pas l’air possible pour l’instant.

Dans la peau d’un objet

name est, comme nous l’avons vu, une variable d’instance. « Instance » est synonyme d’objet. Les variables d’instances sont donc cachées au cœur des objets que nous pouvons créer à loisir, en utilisant un modèle générique, la classe. Mais elles ne sont pas si bien cachées que ça. Pour commencer, vous pouvez les voir si vous inspectez un objet (avec la méthode inspect), et il existe des moyens d’y accéder. Mais Ruby fait en sorte de garder un peu d’ordre dans sa maison, et collant au plus près de l’approche orientée objet, y compris dans la gestion des données et variables qui les renferment.

Quelles sont les méthodes disponibles pour nos instances de la classe Greeter ?

irb(main):039:0> Greeter.instance_methods
=> ["method", "send", "object_id", "singleton_methods",
    "__send__", "equal?", "taint", "frozen?",
    "instance_variable_get", "kind_of?", "to_a",
    "instance_eval", "type", "protected_methods", "extend",
    "eql?", "display", "instance_variable_set", "hash",
    "is_a?", "to_s", "class", "tainted?", "private_methods",
    "untaint", "say_hi", "id", "inspect", "==", "===",
    "clone", "public_methods", "respond_to?", "freeze",
    "say_bye", "__id__", "=~", "methods", "nil?", "dup",
    "instance_variables", "instance_of?"]

Ouch. Voilà une sacré liste de méthodes. Et pourtant, nous n’en avons défini que deux… d’où sortent donc les autres ? En fait, il s’agit d’une liste exhaustive des méthodes applicables aux objets de la classe Greeter, y compris celles définies dans les classes parentes de Greeter. Si nous voulons obtenir la listes des méthodes définies uniquement pour Greeter, il suffit de passer le paramètre false:

irb(main):040:0> Greeter.instance_methods(false)
=> ["say_bye", "say_hi"]

Ok, c’est déjà plus confortable. Et conforme. Vérifions que c’est vrai, en testant quelles méthodes reconnaissent effectivement les instances de Greeter:

irb(main):041:0> g.respond_to?("name")
=> false
irb(main):042:0> g.respond_to?("say_hi")
=> true
irb(main):043:0> g.respond_to?("to_s")
=> true

Une instance de Greeter connaît donc say_hi et to_s (une méthode qui transforme « quelque chose » en une chaîne de caractère et qui est disponible pour tout objet). Par contre, la méthode name est inconnue.

Modifier les classes a posteriori

Mais nous n’en démordrons pas : il nous faut un moyen de récupérer le nom lié à un objet. Comment faire ? Ruby propose un moyen très simple pour accéder aux variables d’instances :

irb(main):044:0> class Greeter
irb(main):045:1>   attr_accessor :name
irb(main):046:1> end
=> nil

Il semblerait que nous ayons défini une seconde fois la classe Greeter… mais il n’en est rien. Nous l’avons simplement « ré-ouverte » et modifiée. Et les changements ainsi définis sont immédiatement disponibles dans tout objet nouvellement créé, ainsi que dans ceux déjà existants ! Créons un nouvel objet et testons l’artifice :

irb(main):047:0> g = Greeter.new("Andy")
=> #<Greeter:0x3c9b0 @name="Andy">
irb(main):048:0> g.respond_to?("name")
=> true
irb(main):049:0> g.respond_to?("name=")
=> true
irb(main):050:0> g.say_hi
Hi Andy!
=> nil
irb(main):051:0> g.name="Betty"
=> "Betty"
irb(main):052:0> g
=> #<Greeter:0x3c9b0 @name="Betty">
irb(main):053:0> g.name
=> "Betty"
irb(main):054:0> g.say_hi
Hi Betty!
=> nil

Le fait d’écrire attr_accessor a implicitement défini deux nouvelles méthodes à peu de frais : name pour récupérer la valeur de la variable/objet name, et name= pour la définir et la modifier.

Accueillir tout et tout le monde !

Le problème avec notre programme actuel, c’est qu’il ne peut s’occuper que d’une seule personne à la fois. Il nous faut vraiment un… SuperProgramme, qui pourrait aussi bien s’occuper de tout le monde en une fois, que d’une seule personne ou de plusieurs personnes à la fois…

Comme notre programme commence à prendre de l’ampleur, arrêtons ici avec IRB, et continuons dans un véritable éditeur de texte. Nous allons enregistrer la suite dans un fichier. Pour quitter IRB, écrivez simplement quit, exit ou appuyez sur [Ctrl]+[D].

Notre fichier va se présenter comme suit :

#!/usr/bin/env ruby

class MegaGreeter
  attr_accessor :names

  # Création d'un objet
  def initialize(names = "World")
    @names = names
  end

  # Saluer tout le monde
  def say_hi
    if @names.nil?
      puts "..."
    elsif @names.respond_to?("each")
      # @names est une liste de noms : traitons-les uns par uns
      @names.each do |name|
        puts "Hello #{name}!"
      end
    else
      puts "Hello #{@names}!"
    end
  end

  # Dire au revoir à tout le monde
  def say_bye
    if @names.nil?
      puts "..."
    elsif @names.respond_to?("join")
      # Grouper les différents noms de la liste par des virgules
      puts "Goodbye #{@names.join(", ")}.  Come back soon!"
    else
      puts "Goodbye #{@names}.  Come back soon!"
    end
  end

end


if __FILE__ == $0
  mg = MegaGreeter.new
  mg.say_hi
  mg.say_bye

  # Modifier le nom en Zeke
  mg.names = "Zeke"
  mg.say_hi
  mg.say_bye

  # Changer le nom pour un tableau (une liste de noms)
  mg.names = ["Albert", "Brenda", "Charles",
    "Dave", "Engelbert"]
  mg.say_hi
  mg.say_bye

  # Maintenant, le nom n'est plus...
  mg.names = nil
  mg.say_hi
  mg.say_bye
end

Sauvegardez ce fichier, par exemple en tant que “ri20min.rb” (l’extension .rb, pour ruby, est importante sous Windows, d’usage sous les autres OS), et éxécutez-le en tapant “ruby ri20min.rb” dans votre terminal. Vous devriez y lire ceci :

Hello World!
Goodbye World.  Come back soon!
Hello Zeke!
Goodbye Zeke.  Come back soon!
Hello Albert!
Hello Brenda!
Hello Charles!
Hello Dave!
Hello Engelbert!
Goodbye Albert, Brenda, Charles, Dave, Engelbert.  Come
back soon!
...
...

Il y a un certain nombre de choses nouvelles dans ce fichier. Examinons-les en détails.

Première nouveauté… la première ligne du bout de code ci-dessous. Elle débute par le caractère # (dièse ou sharp). En Ruby, toute ligne commençant par ce signe est un commentaire, qui sera totalement ignorée par l’interpréteur (IRB compris). Cependant, dans votre fichier, sachez que la toute première ligne (#!/usr/bin/env ruby) est l’inévitable exception à la règle que je viens d’énoncer : son rôle à elle est d’indiquer, sous un système d’exploitation Unix ou apparenté, la bonne façon de traiter le contenu du fichier. En l’occurence, elle signale qu’il s’agit d’un programme à manipuler avec l’interpréteur Ruby, dont elle indique la localisation.

La méthode say_hi est devenue un tout petit peu plus complexe entre temps :

# Saluer tout le monde
def say_hi
  if @names.nil?
    puts "..."
  elsif @names.respond_to?("each")
    # @names est une liste de noms : traitons-les uns par uns
    @names.each do |name|
      puts "Hello #{name}!"
    end
  else
    puts "Hello #{@names}!"
  end
end

Elle jette maintenant un coup d’œil au paramètre @names pour décider de la suite. S’il s’agit de l’absence de valeur signalée par nil, elle écrira trois points « … »—ça ne sert à rien de dire bonjour dans le vide s’il n’y a personne, n’est-ce pas ?

Cycles, loopings… l’itération

Si le paramètre @name, qui est un objet, comme toute chose en Ruby, comprend la méthode each, cela signifie qu’il contient plusieurs choses, et donc qu’il serait intéressant de récupérer ces choses une par unes… de réaliser une itération, en somme. Dernier cas de figure, si @names n’est ni nil, ni une liste à parcourir, alors il s’agit d’une personne unique, et nous pouvons écrire son nom sans plus de manipulation.

Voyons d’un peu plus près cette fameuse itération :

@names.each do |name|
  puts "Hello #{name}!"
end

each est une méthode, un itérateur, qui travaille conjointement avec un bloc de code. Elle fait tourner ce bloc sur chaque élément récupéré dans une liste. Le bloc est délimité par les mot-clés do et end ; il s’agit en quelque sorte d’une fonction qui-ne-dit-pas-son-nom, ou fonction lambda, intégrée au reste du programme. Elle peut prendre des paramètres, qui sont indiqués entre deux caractères pipe |.

Dans notre exemple, pour chaque item de la liste, le paramètre name prend la valeur de l’item en question, de sorte que l’expression puts
"Hello #{name}"
s’applique à ce nom. Et donc, après itération, à tous les noms, dans l’ordre de la liste names.

La plupart des autres langages utilisent un itérateur célèbre, la « boucle for », ce qui donne par exemple en C :

for (i=0; i<number_of_elements; i++)
{
  do_something_with(element[i]);
}

Ce qui fonctionne parfaitement, mais n’est pas spécialement élégant. Vous devez en effet définir une variable locale telle que i, déterminer combien d’itérations vont être effectuées, et expliciter clairement la façon dont la boucle va fonctionner avec le i et le reste… De fait, la façon de faire de Ruby est intrinsèquement plus confortable, puisque que tous ces détails sont l’affaire de la méthode each, qui les garde pour elle. De votre coté, vous ne faites que l’appeler sur un objet de votre choix, et vous vous en remettez à elle pour gérer la mécanique interne. Pour la petite histoire, each consiste essentiellement en un appel à un mot-clé, yield, qui renvoie son résultat au bloc appelant… yield "Albert", yield "Brenda", etc. Mais ce n’est pas l’objet de ce tutoriel.

Les blocs, tout simplement

La réelle puissance des blocs se dévoile dans leur utilisation dans des contextes plus compliqués que la manipulation de listes, pour lesquelles ils sont déjà très pratiques. Ils permettent non seulement de conserver au sein de la méthode le « pourquoi du comment » de son fonctionnement, mais également de gérer personnalisation, destruction et erreurs éventuelles—tout ça loin des yeux de l’utilisateur final de la méthode, qui s’en moque totalement.

# Dire au revoir à tout le monde
def say_bye
  if @names.nil?
    puts "..."
  elsif @names.respond_to?("join")
    # Grouper les noms par des virgules
    puts "Goodbye #{@names.join(", ")}.  Come back soon!"
  else
    puts "Goodbye #{@names}.  Come back soon!"
  end
end

Cette méthode n’utilise pas each, mais la méthode join (si elle est comprise par @names ; sinon, elle écrit juste la valeur de @names, comme dans la méthode précédente). Cette approche consistant à se baser sur ce que la méthode sait faire, plutôt que sur ce qu’elle est (sur les méthodes disponibles plutôt que sur le type de l’objet traité), est désignée par l’expression Duck Typing. Le fameux « si ça marche comme un canard et que ça couine comme un canard, alors c’est un canard. » Le principal bénéfice de cette façon de faire est qu’elle ne limite pas de façon abusive les types de variables supportés. Si quelqu’un décide un jour, par génie ou bêtise, de créer un nouveau type de liste, et écrit une classe pour cela, tant que cette classe implémente la méthode join en collant à la sémantique des autres listes (ie. tant que join fait quelque chose de censé, tout simplement), alors tout fonctionnera parfaitement bien.

La SuperVersion finale du SuperProgramme

Nous voilà donc rendus au bout de l’examen de notre super programme, tirant parti des principales fonctionnalités de base de Ruby. Il contient la classe MegaGreeter, le reste du fichier étant simplement des appels à cette classe, la manipulation d’instances… Il reste un seul point d’ombre :

if __FILE__ == $0

__FILE__ est une variable « magique » qui contient le nom du fichier courant. $0 contient quant à elle le nom du fichier utilisé pour lancer le programme courant. La ligne réalise donc le test « si c’est bien ce fichier que nous utilisons actuellement… », ce qui permet par exemple d’utiliser un fichier comme bibliothèque, en n’exécutant volontairement du code que si le fichier est exécuté, comme un programme autonome.

Bienvenue à bord

Notre rapide tour de Ruby touche maintenant à sa fin… mais il reste encore bien des choses à explorer ! À commencer par les différentes structures de contrôles offertes par Ruby, l’utilisation des blocs et de yield, les modules et les mixins, et plus encore. J’espère que cet avant-goût vous aura donné envie de poursuivre plus avant.

Si tel est le cas, rendez-vous sur notre documentation, qui rassemble des liens vers des manuels et tutoriels disponibles gratuitement en ligne. Si vous êtes tentés par l’achat d’un livre au contenu plus conséquent, vous pouvez consulter ce lien.

Bonne continuation !

Ruby-Lang.org