mardi 19 juillet 2011

Ecrire un DSL en Ruby

Bonjour,


Je m'excuse de pas avoir publié cet article hier ( le lundi ) comme le veut la coutume :) J'étais dans l'embarras du choix du sujet à aborder cette semaine. Et vu que je suis en train de développer une API pour un domaine assez "spécifique" qui a son propre jargon j'ai (enfin) eu une idée : comment écrire un jargon en Ruby.
Dans le monde de l'informatique un jargon est dit DSL.
Les DSL et Ruby
Ruby fournit quelques fantastiques fonctionnalités natives pour la création Domain Specific Languages ​​(DSL). Un Domain Specific Language est comme un langage de programmation écrit pour un domaine déterminé. C'est une façon d'exposer les fonctionnalités dans unformat simple et lisible pour les autres programmeurs (ou vous-même). Un des DSL les plus couramment utilisés dans le monde Ruby est Sinatra.



require 'rubygems'
require 'sinatra'

get '/hello' do
  "Hello world."
end
Sinatra est un DSL pour créer des applications web. Sa syntaxe est construite sur la base des verbes HTTP tels que GET, POST, et PUTEn exposant les fonctionnalités de cette manière, le code est beaucoup plus lisible et beaucoup plus parlant surtout quand on sait comment fonctionne le web en général.


un peu de Yield 

yield est un concept très important à comprendre pour réussir à créer un DSL Ruby. La fonctionnalité fournie par yield permet à un développeur de faire passer le contrôle temporairement pour permettre l'éxécution d'un autre bout de code. Si vous avez déjà utilisé le Array#map (ou Array#collect) qui est un exemple intéressant de l'utilisation de yield. 


[1, 2, 3].map{|i| i + 1} # => [2, 3, 4]
Alors comment pourrions-nous ré-implémenter la fonctionnalité carte si ce n'était pas fait pour nous? C'est en fait assez simple en utilisant yield :

class Array
  def my_map
    resultat = []
    self.each do |truc|
      resultat << yield(truc)
    end
    resultat
  end
end

[1, 2, 3].my_map{|i| i + 1} # => [2, 3, 4]
yield arrête essentiellement l'évaluation(éxécution) de la méthode et évalue le bloc passé, l'appelant avec les arguments fournis dans la déclaration de yield lui-même. Donc, si j'avais une méthode qui simplement retournait  ce qu'on lui passe en arguments, ça ressemblerait à ça :


def confesser( argument )
  yield argument
end

confesser(" Ruby c'est magnifique !") do |arg|
  puts arg
end

# Sortie : "Ruby c'es magnifique !"
Yield et les DSLs


Maintenant, en utilisant yield, nous avons les bases pour créer un DSL simple. Je sais pas pour vous mais moi j'ai faim ! Donc nous allons créer un DSL pour décrire les recettes de cuisine. Nous voulons être en mesure de créer des recettes, d'ajouter des ingrédients ainsi que les étapes, et avoir un joli affichage de tout ça.  Au boulot now :



class Recette
  attr_accessor :nom, :ingredients, :instructions

  def initialize(nom)
    self.nom = nom
    self.ingredients = []
    self.instructions = []
  end

  def to_s
    affichage = nom
    affichage << "\n#{'=' * nom.size}\n\n"
    affichage << "Ingredients: #{ingredients.join(', ')}\n\n"

    instructions.each_with_index do |instruction, index|
      affichage << "#{index + 1}) #{instruction}\n"
    end

    affichage
  end
end

Now ajoutons une recette :


plat = Recette.new("Omelette")

plat.ingredients << "Oeufs"
plat.ingredients << "beurre"
plat.ingredients << "fromage"
plat.ingredients << "autres trucs "

plat.instructions << "Beurre dans poêle"
plat.instructions << "ouefs ..."
plat.instructions << "fromage ..."
plat.instructions << "la suite certainement ..."

Now faisons un "puts plat"! ça doit donner :


Plat
====

Ingredients: Oeufs, beurre, fromage, autres trucs

1) Beurre dans poêle
2) ouefs ...
3) fromage ...
4) la suite certainement ...

Bien que cela fonctionne, le code ne semble pas être élégant! Nous avons besoin d'un moyen de faire ressembler à ce qu'on peut voir sur une carte de recette. Rendons le code un peu plus beau. Pour commencer, nous allons réécrire le constructeur et yield :


def initialize(nom)
  self.nom = nom
  self.ingredients = []
  self.instructions = []

  yield self
end

Next, nous devons ajouter quelques méthodes pour ajouter des ingrédients et des instructions à la recette :


def ingredient(nom, options = {})
  ingredient = nom
  ingredient << " (#{options[:quantite]})" if options[:quantite]

  ingredients << ingredient
end

def instruction(texte, options = {})
  etape = texte
  etape << " (#{options[:pendant]})" if options[:pendant]

  instructions << etape
end

Cela nous permet de créer une recette d'une manière beaucoup plus naturelle :


nouilles_au_fromage = Recette.new("Nouilles au fromage") do |r|
  r.ingredient "Eau",      :quantite => "2 verres"
  r.ingredient "Nouilles", :quantite => "1 verre"
  r.ingredient "Fromage",  :quantite => "3 cuillères"

  r.step "L'eau sur le feu",      :pendant => "5 minutes"
  r.step "Ajouter les nouilles.", :pendant => "6 minutes"
  r.step "Enlever l'eau."
  r.step "Mélanger le fromage aux nouilles."
end

"puts nouilles_au_fromage" donnera :


Nouilles au fromage
===================

Ingredients: au (2 verres), Nouilles (1 verre), Fromage (3 cuillères)

1) L'eau sur le feu. (5 minutes)
2) Ajouter les nouilles. (6 minutes)
3) Enlever l'eau.
4) Mélanger le fromage aux nouilles.

Et voilà ! le petit déjeuner est servi. 
Nous verrons dans la suite comment rendre ce DSL plus joli et plus proche du langages des chefs cuisiniers.


Conclusion :
Nous avons appris à écrire un DSL en Ruby et nous n'avons utilisé qu'une seule méthode ( Yield ). Dans la suite de cet article nous utiliserons d'autres merveilles de Ruby.


Bonne journée

Aucun commentaire:

Enregistrer un commentaire