[version anglaise]

Les nouveaux développeurs Ruby demandent souvent : « Les symboles, c'est quoi, exactement ? Et quelle est la différence entre String et Symbol ? » Malheureusement, il n'y a aucune explication qui va aider tout le monde, alors — avec mes excuses au poète américain Wallace Stevens — je vous présente 13 perspectives sur les symboles de Ruby.

Un symbole de Ruby est :

  1. ...le nom de quelque chose, et pas seulement un morceau de texte
  2. ...une constante dans une énumération informelle
  3. ...un nom constant et unique
  4. ...une chaîne de caractères « internée »
  5. ...un objet qui peut être comparé en temps O(1)
  6. ...un identificateur Lisp
  7. ...un identificateur Ruby
  8. ...le mot-clé d'un paramètre mot-clé
  9. ...un bon choix pour une clé de hachage
  10. ...comme un OSType sur le Mac
  11. ...une fuite de mémoire
  12. ...une façon astucieuse de ne garder en mémoire qu'un seul exemplaire d'une chaîne
  13. ...un typedef C appelé ID

1. Un symbole est le nom de quelque chose, et pas seulement un morceau de texte

En Ruby, on utilise un symbole quand on a besoin d'identifier quelque chose par un nom :

find_text(:parler_pour_ne_rien_dire)

Mais pour représenter des textes, on utilise des chaînes :

"Je vous signale tout de suite que je vais parler pour ne rien dire…"

2. Un symbole est une constante dans une énumération informelle

En C++ (et dans beaucoup d'autres langages), on peut utiliser une énumération pour représenter une famille de constantes apparentées :

enum BugStatus { OPEN, CLOSED };
BugStatus original_status = OPEN;
BugStatus current_status  = CLOSED;

Mais puisque Ruby est un langage dynamique, on n'a pas besoin de déclarer un type BugStatus, ou de maintenir une liste de valeurs permises. À la place, on représente les valeurs de l'énumération avec des symboles :

original_status = :open
current_status  = :closed

3. Un symbole est un nom constant et unique

En Ruby, on peut changer le contenu d'une chaîne :

"foo"[0] = ?b # "boo"

Mais on ne peut pas changer le contenu d'un symbole :

:foo[0]  = ?b # Lève une exception.

De la même façon, on peut avoir deux chaînes différentes avec le même contenu :

# Le même contenu, mais deux chaînes différentes.
"open".object_id != "open".object_id

Mais deux symboles avec le même nom font toujours référence au même objet :

# Le même nom, le même objet.
:open.object_id == :open.object_id

4. Un symbole est une chaîne de caractères « internée »

En Ruby, on peut convertir une chaîne en symbole en utilisant la méthode intern:

"foo".intern # Renvoie :foo

intern maintient une table de hachage qui associe qui associe les chaînes à leurs symboles correspondants. La première fois qu'elle voit une chaîne, elle crée un nouveau symbole, et le met dans la table de hachage. Les fois suivantes, quand intern voit la chaîne, elle cherche le symbole initial dans la table.

Nous pourrions implémenter notre propre version de Symbol et d'intern avec le code suivant :

class MySymbol
  TABLE={}
  def initialize(str) @str = str end
  def to_s() @str end
  def ==(other)
    self.object_id == other.object_id
  end
end

class String
  def my_intern()
    table = MySymbol::TABLE
    unless table.has_key?(self)
      table[self] = MySymbol.new(self)
    end
    table[self]
  end
end

"foo".my_intern

5. Un symbole est un objet qui peut être comparé en temps O(1)

Si on veut comparer deux chaînes, on aura potentiellement besoin de regarder chaque caractère. Dans le cas de deux chaînes de longueur N, il nous faudra N+1 comparaisons, ce que les informaticiens appellent « temps O(N) ».

def string_comp(str1, str2)
  return false if str1.length != str2.length
  for i in 0...str1.length
    return false if str1[i] != str2[i]
  end
  return true
end
string_comp("foo", "foo")

Mais puisque chaque occurrence de :foo se réfère au même objet, on peut comparer deux symboles en examinant leurs identificateurs d'objet. On peut le faire avec une seule comparaison, ce que les informaticiens appellent « temps O(1) ».

def symbol_comp(sym1, sym2)
  sym1.object_id == sym2.object_id
end
symbol_comp(:foo, :foo)

6. Un symbole est un identificateur Lisp

Les ancêtres de symboles Ruby sont les symboles Lisp. En Lisp, les symboles sont utilisés pour représenter les « identificateurs » (les noms de variables et de fonctions) dans un programme après analyse syntaxique. Supposons qu'on a un fichier appelé double.l qui contient une seule fonction :

(defun double (x)
  (* x 2))

On peut analyser ce fichier avec read:

(read "double.l")
;; Renvoie '(defun double (x) (* x 2))

Cette invocation renvoie une liste imbriquée qui contient les symboles defun, double, *, x (deux fois) et le nombre 2.

7. Un symbole est un identificateur Ruby

En Ruby, on peut chercher des identificateurs (les noms de variables, de fonctions et de constantes) pendant l'exécution d'un programme. D'habitude, on le fait en utilisant des symboles.

class Demo
  # Les trucs qu'on va chercher.
  DEFAULT = "Hello"
  def initialize()
    @message = DEFAULT
  end
  def say() @message end

  # Utiliser symboles pour chercher identificateurs.
  def look_up_with_symbols()
    [Demo.const_get(:DEFAULT),
     method(:say),
     instance_variable_get(:@message)]
  end
end

Demo.new.look_up_with_symbols

8. Un symbole est le mot-clé d'un paramètre mot-clé

Quand on passe des arguments mot-clé à une fonction Ruby, on peut spécifier les mots-clés avec des symboles :

# Construire une URL pour l'objet 'bug' avec Rails.
url_for(:controller => 'bug',
        :action => 'show',
        :id => bug.id)

9. Un symbole est un bon choix pour une clé de hachage

D'habitude, on utilise des symboles pour représenter les clés d'une table de hachage :

options = {}
options[:auto_save]     = true
options[:show_comments] = false

10. Un symbole est comme un OSType sur le Mac

Avant l'arrivée de MacOS X (et même un peu après), les anciennes versions du système d'exploitation utilisaient des abréviations de quatre caractères pour représenter des énumérations « extensibles » :

enum {
  kSystemFolderType  = 'macs',
  kDesktopFolderType = 'desk',
  // ...etc...
  kTrashFolderType   = 'trsh'
};
OSType folder = kSystemFolderType;

En Ruby, on utiliserait des symboles dans un contexte similaire :

:system_folder
:desktop_folder
:trash_folder

11. Un symbole est une fuite de mémoire

En raison de la manière dont sont stockés les symboles Ruby, ils ne peuvent jamais être recyclés par le ramasse-miettes [en]. Alors, si on crée 10 000 symboles qu'on n'utilisera plus jamais, on ne récupèra jamais la mémoire.

Certaines implémentions de Scheme utilisent une version astucieuse d'intern qui stocke des symboles dans une table de hachage à référence faible. Cela permet de ramasser les symboles sans perdre leurs propriétés d'unicité.

12. Un symbole est une façon astucieuse de ne garder en mémoire qu'un seul exemplaire d'une chaîne

(Pour une idée similaire, voir cet article [en].)

Supposons qu'on travaille sur un analyseur de langage naturel qui essaye de comprendre des commandes de petit déjeuner. On a un corpus de 30 000 phrases qui représentent les commandes d'un restaurant typique, et on va essayer de trouver les motifs.

Mais même si on a un très grand nombre de phrases, le vocabulaire lui-même est très limité. On ne veut pas garder 15 000 exemplaires du mot « bacon » en mémoire ! À la place, on peut utiliser des symboles pour représenter les mots individuels :

corpus = [
  [:i, :want, :some, :bacon],
  [:i, :want, :some, :eggs],
  [:give, :me, :some, :bacon],
  [:chunky, :bacon],
  # ... 29 995 phrases en plus ...
  [:some, :toast, :please]
]

Aux débuts de l'IA, beaucoup de programmes Lisp utilisaient la même stratégie pour représenter des textes.

13. Un symbole est un typedef C appelé ID

Au niveau interne, Ruby 1.8 représente des symboles avec le type ID. Cela est un typedef pour un entier non-signé. (C'est un peu plus compliqué avec la version 2.1, mais l'idée reste la même.) Un ID représente une entrée dans la table de symboles de Ruby.

typedef unsigned long ID;

Voici quelques fonctions intéressantes qui concernent les symboles :

// Entrer une chaîne C dans la table de symboles.
ID rb_intern(const char *name);
// Convertir un ID en objet symbole.
#define ID2SYM(x)
// Convertir une chaîne en objet symbole.
VALUE rb_str_intern(VALUE s);

Autres explications de symboles Ruby

Si aucune de ces explications ne vous aide, peut-être aurez-vous plus de chance avec l'une de celles-ci :

  1. Expliquez-moi : les symboles du langage Ruby
  2. Symbols Are Not Immutable Strings
  3. Using Symbols for the Wrong Reasons
  4. Yet Another Blog About Ruby Symbols
  5. Digging into Ruby Symbols
  6. Understanding Ruby Symbols

(Merci à Alex, Lisa et Henri pour leurs corrections.)