Go to Hackademy website

Ruby 3, les nouveautés

Nicolas Cavigneaux

Posté par Nicolas Cavigneaux dans les catégories ruby

Cet article est publié sous licence CC BY-NC-SA

Le 25 décembre 2020, la version 3.0 de Ruby a officiellement été publiée. Beaucoup de Rubyistes l’attendaient avec impatience tant elle s’annonçait prometteuse.

Voyons aujourd’hui ce qu’elle nous apporte d’excitant à utiliser.

Performances

La nouveauté la plus discutée depuis longtemps concernant cette nouvelle version était le fameux « Ruby 3 × 3 » qui promettait que Ruby 3 serait 3 fois plus performant que Ruby 2. Je dis dit bien que Ruby 2 et pas Ruby 2.x parce que les différentes améliorations de performance ont été distillées tout au long du développement de Ruby 2.x.

Cette promesse a été tenue et les performances sont au rendez-vous. Cette promesse a toujours été relative à un cas d’usage bien précis qui servirait de référence tout au long du développement. Il s’agissait de faire tourner un émulateur NES à 60 FPS. Il faut savoir que Ruby 2.0 n’arrivait qu’à 20 FPS.

Ces gains de performances sont dus à de nombreuses optimisations mais surtout à la mise en place du compilateur JIT. JIT signifie « Just In Time » compilation qui est une technique fréquemment utilisée pour améliorer les performances d’un langage. Il s’agit de faire en sorte que le code applicatif puisse être compilé pendant son exécution, au runtime. C’est une approche dynamique de la compilation classique des sources. On a donc, à la fois, du code interprété et compilé, ce qui permet d’avoir le meilleur de deux mondes : un code rapide à exécuter mais qui reste très flexible et dynamique.

Mémoire

Le compactage du garbage collector avait été ajouté dans Ruby 2.7.

Il faut distinguer ici garbage collection et garbage compaction. Le premier permet de nettoyer les allocations redondantes en mémoire pour gagner de l’espace et pouvoir en allouer à de nouvelles choses. Quant à celui qui nous intéresse ici, le compactage, il regroupe les objets éparpillés dans la mémoire, laissant ainsi des espaces mémoire assez gros pour en allouer à des objets plus lourds plus facilement.

Il était possible de le déclencher manuellement en invoquant GC.compact. Ruby 3 déclenche désormais ce compactage de manière totalement automatisée. Le compactage se fait aux moments les plus adéquats pour assurer que la mémoire reste la plus défragmentée possible tout au long de l’exécution de l’application.

Il n’est donc plus nécessaire de le gérer à la main pour assurer les meilleures performances possibles.

Typage

Ruby n’est pas un langage typé. Ce choix a été fait pour permettre aux développeurs de pouvoir prototyper plus rapidement et plus facilement.

Ceci étant, il est parfois bien utile de pouvoir typer son code pour gagner en robustesse. C’est notamment vrai lorsqu’il s’agit de maintenir de grosses applications.

Avec Ruby 3, il est maintenant possible de jouer un peu dans les deux camps. La notion de vérification statique de type (static type checking) a officiellement été intégrée.

On peut donc, si on le souhaite, documenter ses classes et méthodes pour faire connaître à l’interpréteur vos intentions quant à l’utilisation de votre code. Grâce à cela il est possible de découvrir des incohérences beaucoup plus tôt.

Ce système de typage s’appelle RBS et peut donc être utilisé pour décrire le fonctionnement de vos classes, méthodes, variables d’instance, modules et constantes.

La description du typage de votre code ne se fait pas directement dans le code source Ruby. Les signatures sont stockées dans des fichiers .rbs ce qui permet de typer une application existante après coup sans avoir à en modifier son code source. Cette fonctionnalité est donc rétro-compatible.

Voici un exemple de déclaration :

class Message
  attr_reader id: String
  attr_reader string: String
  attr_reader from: User | Bot
  attr_reader reply_to: Message?

  def initialize: (from: User | Bot, string: String) -> void

  def reply: (from: User | Bot, string: String) -> Message
end

Cette nouvelle fonctionnalité nous permettra de

  • trouver plus facilement des bugs (comme des undefined, des valeurs nil là où c’est théoriquement impossible…), Il est également possible de déclarer des interfaces
  • optimiser l’intégration dans les éditeurs en améliorant la complétion, en reportant les erreurs en temps réel ou encore en aidant au refactoring
  • mettre en place du duck typing de manière plus sûre grâce à la déclaration d’interfaces

Parallélisme et concurrence

La gestion de la concurrence en Ruby a toujours été un sujet sensible. Jusque-là nous ne pouvions qu’utiliser les threads qui ont malheureusement beaucoup de défauts et sont assez difficiles à gérer correctement.

Ruby 3 nous apporte de nouvelles façons de mettre en place de la concurrence.

Fibers

Les fibers sont une alternative légère aux threads. Elle permet la mise en place de « concurrence coopérative ».

Elles consomment moins de mémoire et permettent un contrôle plus fin que les threads.

Ce n’est plus la VM qui décide de quand un morceau de code concurrent doit être arrêté et repris. On laisse le développeur s’en charger.

Ruby 3 apporte le Fiber Scheduler qui est capable d’intercepter les opérations bloquantes (I/O) et de les jouer de manière concurrente. On peut donc avoir une boucle d’événements qui sera séparée du code applicatif.

Le scheduler est une interface, il faut donc l’intégrer dans un wrapper. Des gems telles que Async ou EventMachine intègrent déjà cette interface.

On peut donc, par exemple, télécharger plusieurs fichiers en même temps avec de la vraie concurrence.

puts "1 : c'est parti"

# On crée une nouvelle fibre
f = Fiber.new do
  puts "3: on entre dans la fibre."
  Fiber.yield # On met la fibre en pause
  puts "5: la fibre a été redémarrée."
end

puts "2: on démarre la fibre"
f.resume

puts "4: on reprend là ou la fibre s'était arrêtée."
f.resume

puts "6: c'est fini."

Ractors

Le verrou global qui existe sur la VM Ruby nous empêche d’avoir des threads Ruby (green threads) qui travaillent en parallèle. L’utilité globale des threads est donc assez limitée.

Ractor est une réponse à cette problématique. Ractor se base sur le modèle Acteur que les utilisateurs d’Elixir connaissent bien.

Ractor est aussi lourd en termes de ressource que les threads mais a l’avantage de ne pas souffrir du verrou global de la VM. Chaque Ractor s’exécute en parallèle.

Avec ce modèle il devient beaucoup plus facile d’être thread safe. Le fonctionnement même de ce modèle incite à écrire les tâches concurrentes d’une manière qui évite ce problème. Les informations ne sont pas partagées entre Ractor contrairement aux threads. Dans le modèle Acteur, les différents acteurs se communiquent les informations en s’échangeant des messages.

ractor1, ractor2 = *(1..2).map do
  Ractor.new do
    arg = Ractor.receive
    "opération longue #{arg}"
  end
end

# On envoie le paramètres à nos instances
ractor1.send 1
ractor2.send 2

p ractor1.take #=> "opération longue 1"
p ractor2.take #=> "opération longue 2"

Pattern matching

Le pattern matching avait été mis à disposition de manière expérimentale avec Ruby 2.7 mais est officiel depuis Ruby 3.

Le pattern matching permet d’associer automatiquement des valeurs d’une structure à des variables (si la structure passée correspond à l’attendu).

Pour ceux qui avaient déjà commencé à l’utiliser, sachez que la syntaxe a changée :

# Ruby 2.7
{ name: "Jon", role: "CTO" } in {name:}
p name # => 'Jon'

# Ruby 3.0
{ name: "Jon", role: "CTO" } => {name:}
p name # => 'Jon'

On peut également utiliser le pattern matching dans les case :

users = [
  { name: "Jon", role: "CTO" },
  { name: "Marcel", role: "Manager" },
  { role: "Client" },
  { name: "Lucie", city: "Lille" },
  { name: "Nico" },
  { city: "Paris" }
]

users.each do |person|
  case person
  in { name:, role: "CTO" }
    p "#{name} est le CTO."
  in { name:, role: designation }
    p "#{name} est #{designation}."
  in { name:, city: "Lille" }
    p "#{name} vit à Lille."
  in {role: designation}
    p "Un inconnu est #{designation}."
  in { name: }
    p "#{name} n'a pas de rôle."
  else
    p "Aucun pattern ne correspond."
  end
end

"Jon est le CTO."
"Marcel est Manager."
"Un inconnu est client."
"Lucie vit à Lille."
"Nico n'a pas de rôle."
"Aucun pattern ne correspond."

Définition courte de méthode

Un sucre syntaxique a été ajouté permettant d’avoir une syntaxe concise lorsqu’il s’agit de définir une méthode courte.

def: foo(bar) = puts bar

foo("ruby") #=> "ruby"

except

Pour les nombreux d’entre vous qui utilisent Rails, vous avez déjà sûrement utilisé la méthode except fourni par ActiveRecord et qui permet d’obtenir un Hash dénué d’un ou plusieurs de ses éléments.

Cette méthode a été intégrée directement à Ruby !

user = { name: "Jean", city: "Lille", role: "Dev" }
user.except(:role) #=> {:name=> "Jean", :city=> "Lille"}

Conclusion

Comme vous pouvez le voir, les nouveautés intéressantes sont nombreuses. Elles devraient encore nous faciliter la vie et nous permettre d’écrire du code avec toujours autant de plaisir.

De nouveaux outils sont mis à notre disposition et nous ouvrent de nouvelles portes notamment côté performances et concurrence.

Je n’ai fait part dans cet article que des fonctionnalités qui m’ont le plus marquées, mais je vous invite à consulter la liste des changements si vous voulez connaître l’ensemble des changements qui ont eu lieu.

Je vous conseille également de lire cette interview de Matz qui nous parle des nouveautés de Ruby 3 et notamment de comment faire le choix entre Ractor et Fiber lorsqu’on a besoin de mettre en place de la concurrence.


L’équipe Synbioz.
Libres d’être ensemble.

Articles connexes

Gosu et sa boucle principale

01/04/2021

Contrôler la boucle principale de Gosu L’idée de cet article est d’expliquer pourquoi on peut vouloir prendre le contrôle sur la boucle principale de notre moteur de jeu et comment on peut y arriver...

Un meilleur rendu de Git diff pour Ruby

18/02/2021

Si vous utilisez Git en ligne de commande, vous connaissez bien la sortie diff qui se décompose en morceaux (hunks) comme ci-dessous :

Introduction à DragonRuby

22/01/2021

DragonRuby Game Toolkit DragonRuby est un moteur de jeu 2D développé par Ryan C. Gordon, l’un des principaux développeurs de la SDL et Amir Rajan un développeur de jeu vidéo reconnu pour avoir...

Principe du parapluie

15/01/2021

Il est possible de penser juste avec des choses qui n’existent pas. À vrai dire, c’est même le propre de l’informatique que de penser des choses qui n’existent pas. C’est-à-dire des choses abstraites.