Ecriture d'un DSL en Ruby
Par Bounga le lundi, 22 juin 2009, 12:16 - Documentations - Lien permanent
Beaucoup d'entre vous connaissent Ruby grâce à son fort potentiel Web au travers du framework Ruby on Rails. Mais Ruby excelle dans de nombreux domaines et s'avère particulièrement efficace dans l'écriture de DSL (Domain Specific Language).
Les DSL existent depuis toujours et vous les utilisez peut-être même sans vous en rendre compte. C'est un concept très à la mode ces derniers temps parce qu'avec les langages modernes, il n'a jamais été aussi facile d'en développer un.
Le but d'un DSL (du moins en Ruby) est de proposer à l'utilisateur un langage simple qui va lui permettre d'accomplir des tâches ciblées. L'utilisateur écriera du Ruby sans même s'en rendre compte.
Nous allons voir dans ce billet comment tirer parti de ces possibilités et créer notre propre DSL.
Jeu de questions / réponses
Nous allons pour commencer, mettre en place un exemple simple. Nous allons écrire un logiciel de quiz qui va nous permettre de donner une liste de questions et pour chaque question nous pourrons donner des réponses erronées et une réponse correcte.
Notre programme, côté utilisateur, fonctionnera comme suit :
Quel est le langage utilisé par Ruby on Rails ?
1 - C#
2 - Python
3 - Ruby
4 - PHP
Quel est votre réponse ?
L'idée derrière ce générateur de Quiz est d'avoir un mini-langage dédié à l'écriture des questions et de ses réponses. Le programme que nous écrirons sera en mesure de lire ce mini-langage pour l'exécuter via l'interpréteur Ruby. Nous aurons donc des fichiers de quiz qui décrirons les questions / réponses :
question 'Quel est le langage utilisé par Ruby on Rails ?'
faux 'C#'
faux 'Python'
vrai 'Ruby'
faux 'PHP'
Ces questions pourront être dans un fichier 'mon_test.quiz' par exemple. Il faut comprendre que ce fichier ne sera pas réellement un fichier de données mais plutôt un programme qui sera interprété par Ruby.
Les appels à 'question', 'vrai' et 'faux' seront en fait des appels à des méthodes Ruby que nous allons définir. Nous ne parserons pas un fichier texte pour en déduire quelque chose, nous éxecuterons un vrai programme en Ruby.
Lire les questions
Nous allons commencer par implémenter la lecture de notre fichier de questions, voici ce à quoi devrait ressembler quiz.rb :
#!/usr/bin/env ruby def question(text) puts "Voici une question: #{text}" end def vrai(text) puts "Voici une réponse valide: #{text}" end def faux(text) puts "Voici une réponse incorrecte: #{text}" end load 'mon_test.quiz'
Pas beaucoup de code me direz-vous, et bien oui mais pourtant avec si peu de code, nous avons déjà un programme fonctionnel qui va être capable de lire les questions et les réponses. Vous avez, avec ce petit morceau de code, toute la logique d'écriture d'un DSL sous les yeux. Nous avons simplement définit 3 méthodes qui vont nous permettre de lire notre fichier de quiz.
Il ne nous reste plus qu'à lire notre fichier de quiz :
load 'mon_test.quiz'
Ici encore, nous utilisons du code Ruby conventionnel qui nous sert à charger un fichier qui sera interprété comme étant du code Ruby. Notre fichier 'mon_test.quiz' n'est en fait rien d'autre qu'un fichier Ruby qui fait de multiples appels aux méthodes 'question', 'vrai' et faux' que nous avons définit avant.
Si nous lancions notre programme, nous obtiendrions :
Voici une question: Quel est le langage utilisé par Ruby on Rails ?
Voici une réponse incorrecte: C#
Voici une réponse incorrecte: Python
Voici une réponse valide: Ruby
Voici une réponse incorrecte: PHP
Création d'un quiz
Maintenant que nous sommes capable de lire les questions et réponses, il ne nous reste plus qu'à implémenter le fonctionnement de chaque méthode. Il faut donc décrire ce que feront réellement les appels à 'question', 'vrai' et faux'.
Dans notre exemple de quiz, cela reste très facile à faire puisqu'il s'agit simplement de mettre en place une structure de données. Nous allons donc créer une classe Quiz qui va nous permettre d'encapsuler le fonctionnement.
class Quiz def initialize @questions = [] end def ajouter_question(question) @questions << question end def derniere_question @questions.last end def lancer count=0 @questions.each { |q| count += 1 if q.poser } puts "Vous avez #{count} bonnes réponses sur #{@questions.size}." end end
Cette classe va simplement nous servir à créer une collection de questions. Il nous faut maintenant une classe Question qui va nous permettre de poser une question et de savoir si la réponse donnée est bonne ou non.
class Question def initialize(text) @text = text @reponses = [] end def ajouter_reponse(reponse) @reponses << reponse end def poser puts "" puts "Question: #{@text}" @reponses.size.times do |i| puts "#{i+1} - #{@reponses[i].text}" end print "Quel est votre réponse ? " reponse = gets.to_i - 1 return @reponses[reponse].correct end end
Une question représente simplement une chaîne de caractères associées à un ensemble de réponses.
Pour finir, nous aurons besoin d'une classe Reponse pour gérer les réponses utilisateur :
class Reponse attr_reader :text, :correct def initialize( text, correct ) @text = text @correct = correct end end
Nous n'avons plus qu'à rassembler ces éléments pour avoir un programme fonctionnel.
Et voici la touche finale qui nous permettra d'avoir un programme utilisable :
#!/usr/bin/env ruby require 'quiz' @quiz = Quiz.new def question(text) @quiz.ajouter_question Question.new(text) end def vrai(text) @quiz.derniere_question.ajouter_reponse Reponse.new(text,true) end def faux(text) @quiz.derniere_question.ajouter_reponse Reponse.new(text,false) end load 'mon_test.quiz' @quiz.lancer
La méthode 'question' nous permet d'ajouter un nouvel objet Question dans notre instance de Quiz. Les réponses sont collectées (pour la dernière question posée) par les méthodes 'vrai' et 'faux'. Ces deux méthodes sont identiques, seul un booléen nous permet de savoir si la réponse est correcte ou non.
Les 2 dernières lignes de notre programme nous permettent de charger les questions et de lancer le quiz. Nous aurions d'ailleurs pu mettre en place une routine qui chargerait tous les quiz présents dans un répertoire ou faire en sorte que le quiz soit un paramètre de la ligne de commande.
Si vous tester quiz.rb, vous devriez obtenir quelque chose de ce genre :
Question: Quel est le langage utilisé par Ruby on Rails ?
1 - C#
2 - Python
3 - Ruby
4 - PHP
Quel est votre réponse ? 3
Vous avez 1 bonnes réponses sur 1.
Cet exemple représente parfaitement la structure d'un DSL typique.
Nous avons commencer par définir la structure d'un quiz du plus général au plus spécifique :
- Quiz
- Question
- Reponse
Nous avons ensuite définit quelques méthodes de premier niveau (accessibles directement depuis Object) qui vont nous permettre d'utiliser notre DSL (méthodes 'question', 'vrai', faux'). Après quoi nous pouvons charger les questions / réponses utilisateur (load 'mon_test.quiz') pour finalement lancer le test.
Avantages et inconvénients des DSLs internes
Les DSLs internes (le fichier de donnés est écrit dans le même langage que son interpréteur) ont des avantages certains :
- concision (70 lignes) et clarté du code DSL
- Possibilité d'utiliser les capacités de ruby depuis le fichier de données pour ajouter des commentaires dans le fichier de données, générer des questions posées au hasard, afficher les réponses dans un ordre aléatoire, implémenter de nouvelles fonctionnalités avancées
Voici un exemple d'une utilisation avancée de Ruby dans le fichier de données :
a = rand() b = rand() question "Quel est la somme de #{a} et #{b}" faux "#{a + b + 12}" faux "#{a + (b * 2)}" faux "#{a + b - 0.5}" vrai "#{a + b}"
Il y a bien évidemment des inconvénients à utiliser un DSL interne. L'avantage cité ci-dessus pourrait vite devenir un désanvatage si vous deviez limiter les possibilités côté utilisateur. L'utilisateur est ici en mesure (pour le peu qu'il connaisse Ruby) de faire tout ce que bon lui semble dans le fichier de configuration des questions / réponses. Il lui serait donc possible d'ouvrir des connexions réseau, d'écrire des fichiers, …
Voici qui clôture cette introduction sur les DSL en Ruby. Avez-vous déjà écrit des DSL ? Voyez-vous des applications concrètes et utiles de DSL ?
Bon amusement.