Follow Us on Twitter

Internal DSL in Ruby

Augustus 2008 - Het -vermoedelijk- belangrijkste wat we doen als softwareconsultants en -ontwikkelaars, is het omzetten van eisen van de klant naar een formele taal als Java, PHP, C++ of Ruby. Dit proces gaat meestal in stappen, via meerdere rollen of mensen. Een business-consultant praat met de klant om een globale eisen op papier te krijgen. Een functioneel ontwerper maakt een coherent functioneel ontwerp aan de hand van de eisen van de klant. Tot slot schrijft een programmeur aan de hand van het functioneel ontwerp een programma. De echt allerlaatste stap is het omzetten van de programmeertaal in machinecode of bytecode. Alleen deze stap wordt nog maar zelden door een mens gedaan.

Het resultaat van iedere vertaling ligt qua begrijpelijkheid verder af van de klant, ofwel het domein van de klant. De programmacode bevat business logica zoals die gedurende ontwikkeling is vastgelegd, maar deze logica is verscholen in de software. Hierdoor zijn latere aanpassingen en uitbreidingen van de software behoorlijk moeilijk: de oorspronkelijke ontwikkelaars zijn vaak niet meer beschikbaar na enige jaren, of hun kennis is weggezakt. De ontwikkelaars hebben een andere baan of de aanpassing van de software is uitbesteed aan een ander bedrijf. De meeste software is matig gedocumenteerd, en de programmeertaal of framework waarin de software geschreven is kan inmiddels verouderd of uit de gratie geraakt zijn. Gevolg is dat de enige manier om de software uit te breiden, herschrijven is. Hoeveel opdrachten voor nieuwe projecten beginnen met 'het systeem x is verouderd en moet herschreven worden'? Alle kennis die de analysten, consultants, programmeurs hebben vergaard gedurende de ontwikkeling is grotendeels verloren gegaan omdat deze verscholen is in vele regels in C++, Cobol, VBA of Java.

How the analysts designed it
How the analysts designed it (uit de cartoon "How Projects Work")

Om deze afstand tussen oorspronkelijke domein en de programmacode te verkleinen, kan een DSL gebruikt worden: Domain Specific Language. Zoals de naam zegt, een DSL is een niet-ambigue taal om een specifiek domein van de wereld te beschrijven, in tegenstelling tot een generieke (niet-ambigue) taal als Java, Basic of HTML, die voor ieder willekeurig domein gebruikt kan worden. Met een niet-ambigue taal wordt hier (globaal) bedoeld, dat er een definitie van de taal beschikbaar is, zodat de taal automatisch verwerkt kan worden door een computerprogramma. In de toekomst zal het ongetwijfeld mogelijk zijn om de computer met natuurlijke taal te instrueren om zelf software te schrijven, maar zover is de stand van de wetenschap nog niet.

Op eerste gezicht lijkt gebruik van een DSL complex en een risico: het is al moeilijk genoeg om bij voorbeeld Java-programmeurs of PHP-developers te vinden, waarom een taal schrijven die alleen bekend is binnen een beperkt domein? Deze complexiteit is maar schijn. Binnen ieder softwareproject ontstaat al snel een jargon, of ruimer gezegd, een eigen taal. Voor buitenstaanders is die taal onbegrijpelijk, voor mensen binnen project onontbeerlijk om te kunnen werken. Kortom, er is al een redelijke leercurve van het leren van een nieuwe taal, voor wie nieuw begint met programmeren binnen een softwareproject.

Het meest flexibel, maar ook het moeilijkst, is het om een DSL van de grond af aan op te bouwen. Heel kort samengevat komt het schrijven van een taal op het volgende neer: Eerst moet een definitie gemaakt worden. Het opzetten van een taal bestaat uit het schrijven taaldefinitie zoals BNF (en.wikipedia.org/wiki/Backus-Naur_form) of XML-Schema (www.w3.org/XML/Schema), vervolgens kan met behulp van software zoals een compilergenerator een compiler gemaakt worden, die de taal kan omzetten in software die door een computer kan worden uitgevoerd. Het ontwerp van een goede generieke programmeertaal vergelijkbaar met Java, C++ of Ruby is een proces wat voor de meeste programmeurs, qua tijd en en kunde niet weggelegd is, zeker niet voor een beperkt domein.

Sinds de opkomst van XML, is het schrijven van eigen - op XML-gebaseerde - talen een stuk makkelijker geworden. Gestandaardiseerde software wordt tegenwoordig meegeleverd met de Java VM of .Net software, om XML te kunnen uitlezen, vertalen en te verwerken.

Enkele voorbeelden van DSL's gebaseerd op XML zijn: COIN (standaard communicatie tussen telecomproviders), RuleML (standaard voor beschrijven regels), SCORM (voor vastleggen van e-learning content). Andere DSL's zijn: Makefiles (voor gebruik binnen ontwikkelproces), verschillende workflowsystemen (een 'grafische' DSL) en LaTeX (voor schrijven van opgemaakte .tekstdocumenten).

Al deze genoemde talen zijn voorbeelden van external DSL's (Martin Fowler, zie referenties): talen die op zichzelf staan. Voordeel is, zoals genoemd de flexibiliteit. Nadeel is, het ontwikkelen van zo'n taal is erg moeizaam. De taal moet genoeg expressiekracht hebben, programmeurs zullen anders veel logica alsnog in programmacode moeten vangen, waardoor het nut van de DSL is verdwenen. Tot slot moet iedereen binnen de 'business' overtuigd worden de taal te gebruiken, wat nogal wat politiek werk oplevert. Een complete taal van de grond af opbouwen is veel werk.

Er is een makkelijke manier: het schrijven internal DSL, een DSL in binnen bestaande programmeertaal. Het schrijven van een compiler is niet langer nodig, de leercurve is lager en flexibiliteit is vele malen hoger. Internal DSL's kunnen in iedere programmeertaal die flexibel genoeg is geschreven worden. Ook Java is hiervoor geschikt. Een bekend evangelist van gebruik van DSL's, Neal Ford heeft een goede beschrijving hoe een DSL in Java eruit kan zien, zie de referenties voor meer informatie. In dit stuk zullen we beschrijven hoe een DSL geschreven kan worden in een buitengewoon dynamische taal: Ruby. Aan de hand twee voorbeelden wordt uitgelegd hoe binnen de taal Ruby eenvoudig een DSL geschreven kan worden.

Een type DSL die iedere ontwikkelaar regelmatig gebruikt zijn de talen van build-files. De meest populaire build-software binnen de Java-wereld is Ant, andere bekende build-software is Make. Ant of Make ook gebruikt kunnen worden binnen Ruby projecten, maar voor een Ruby-programmeur is het fijner om build-files te kunnen schrijven die dichter bij de taal Ruby zelf ligt: Rake. Rake is geschreven in Ruby en biedt het volledige gamma van de taal ter ondersteuning van het schrijven van build scripts. Ant is een voorbeeld van een DSL op basis van een XML syntaxis en make maakt gebruikt van een eigen syntaxis.

Rake is een DSL voor het maken van builds. Het verschil met Ant en make is het soort DSL. Rake is namelijk een internal DSL, waar ant en make van het type external DSL zijn (zie Martin Fowler).

Een simpel voorbeeld van een rake script om een java bestand te compileren en uitvoeren:

Rakefile

#De code tussen do – end zijn ruby blocks. Blocks zijn closures binnen 
#ruby. In dit voorbeeld is het voor beide taken de tweede parameter. 

#compile heeft geen afhankelijkheden 
task :compile do 
       sh "javac HelloWorld.java" 
end 

#run is afhankelijk van compile 
task :run => :compile do 
       sh "java HelloWorld" 
end 
  
#Hier wordt de block voorzien van een referentie naar de taak. Binnen 
#de block kan de taak gemanipuleerd worden. Dit bevorderd de 
#leesbaarheid aanzienlijk en biedt vele bijzonder krachtige extra’s. 
task :run_2 => 'HelloWorld.class' do |t| 
       sh "java #{t.prerequisites[0].slice(/(.*)\./,1)}" 
end 
  
#File is ook een vorm van task, alleen wordt hier duidelijker aangeven
#dat de taak tot een of meerdere bestanden leidt. Tevens controleert 
#dit type taak of het bestand al bestaat. Dit kan echter ook 
#gemakkelijk gerealiseerd worden binnen een normale taak via 
#de uptodate? Methode. 
file 'HelloWorld.class' => 'HelloWorld.java' do |t| 
       sh "javac #{t.prerequisites[0]}" 
end 

HelloWorld.java

public class HelloWorld { 
    public static void main(String[] args) { 
        System.out.println("Hello world!"); 
    } 
} 

Uitvoer 

Jruby –S rake run 
(in C:/examples) 
javac HelloWorld.java 
java HelloWorld 
Hello world! 
  
Jruby –S rake run2 
(in C:/examples) 
java HelloWorld 
Hello world!

Hier valt op dat bij het uitvoeren van run2 HelloWorld niet meer gecompileerd wordt. Dit komt omdat er gebruik wordt gemaakt van een file task.

De volgende voorbeelden beschrijven een internal DSL geschreven in Ruby. Met deze DSL is het mogelijk om een basale relatie tussen studenten en vakken te leggen op een school. Eerst wordt er een eenvoudig objectmodel gebouwd, vervolgens een interpreter voor de DSL en afsluitend een diverse commando’s voor de DSL.

Het objectmodel voor de school, studenten en vakken:

require 'singleton' 
class School 
  include Singleton 
  attr_reader :students 
  attr_accessor :courses 
  
  def initialize 
    @courses = {} 
    @students = {} 
  end 
  
  def add_course (course_name) 
    @courses[course_name] = Course.new(course_name) 
    puts "Course #{course_name} added." 
  end 
  
  def add_student (student_name) 
    @students[student_name] = Student.new(student_name) 
    puts "Student #{student_name} added." 
  end 
  
  def add_student_to_course (student_name, course_name) 
    student = @students[student_name] 
    course = @courses[course_name] 
    course.followed_by(student) 
    puts "Student #{course_name} now follows #{course_name}." 
  end 
  
  def show_course_details (course_name) 
    course = @courses[course_name] 
    puts "Course: " + course.name 
    puts "Followed by: " 
    course.students.each do |student_naam, student| 
      puts "  #{student.name}" 
    end 
    puts 
  end 
  
  def show_student_details (student_name) 
    student = @students[student_name] 
    puts "Student: " + student.name 
    puts "Follows the following courses: " 
    @courses.select {|key, course| course.students[student.name]}.each 
      do |key, course| 
      puts "  #{course.name}" 
    end 
    puts 
  end 
  
  def method_missing(m, *args)  
    if m.to_s =~ /show_.*_details/ 
      methods = self.methods.select 
        {|method| method =~ /show_(.*)_details/} 
      puts "The following options are available for show_info_for" 
      methods.each do |method| 
        puts ":#{/show_(.*)_details/.match(method)[1]}" 
      end 
      puts 
    else 
      puts "The method you requested is not available."  
    end 
  end  
end 
  
class Course 
  attr_reader :name 
  attr_reader :students 
  
  def initialize (name) 
    @name = name 
    @students = {} 
  end 
  
  def followed_by(student) 
    @students[student.name] = student 
  end 
end 
  
class Student 
  attr_reader :name 
  def initialize(name) 
    @name = name 
  end 
end 

De DSL interpreter:

def new_course(options) 
  School.instance.add_course(options[:name]) 
end 
  
def course(options) 
  School.instance.add_student_to_course(options[:followed_by], 
    options[:name]) 
end 
  
def new_student(options) 
  School.instance.add_student(options[:name]) 
end 
  
def show_info_for(options) 
  option = options.shift  
  School.instance.send "show_#{option[0]}_details", option[1] 
end 

DSL voor het manipuleren van het school objectmodel:

new_course :name => 'Economie - I' 
new_course :name => 'Wiskunde A' 
  
new_student :name => 'Jan' 
new_student :name => 'Pieter' 
new_student :name => 'Naomi' 
  
course :name => 'Economie - I', :followed_by => 'Jan' 
course :name => 'Economie - I', :followed_by => 'Pieter' 
course :name => 'Wiskunde A', :followed_by => 'Pieter' 
course :name => 'Wiskunde A', :followed_by => 'Naomi' 
  
show_info_for :course => 'Economie - I' 
show_info_for :course => 'Wiskunde A' 
  
show_info_for :student => 'Jan' 
show_info_for :student => 'Pieter' 
show_info_for :student => 'Naomi' 
  
show_info_for :teacher => 'Jan' 

De kracht van een DSL binnen een taal als Ruby is de goed leesbare syntaxis, zo is het bijvoorbeeld niet verplicht om haakjes te gebruiken en kan je eenvoudig arrays als parameters meesturen. Het dynamische sterk getypeerde model en de blocks (closures) van Ruby maken de code compact, krachtig en goed leesbaar.

Het principe van DSL's bestaat al vele tientallen jaren, heeft nooit breed aangeslagen. Met de opkomst van dynamische en flexibele programmeertalen en computers met enorme capaciteit, en vooral steeds grotere en complexere softwareprojecten komt het gebruik van DSL enorm in opkomst. Door gebruik van een internal DSL binnen een bestaande programmeertaal, is de introductie van een DSL minder ingrijpend geworden en zal sneller geaccepteerd worden. Samen met andere methodieken zoals Service Component Architecture en functional programming kunnen DSL's voor een ingrijpende en positieve verandering van werken binnen softwareontwikkeling zorgen.

 

Referenties:

Over de auteurs
Barry van de Graaf is Java Consultant bij Whitehorses en heeft 5 jaar ervaring in de IT. Zijn primaire focus ligt op het gebied van webapplicaties in diverse Java frameworks en heeft zich in het afgelopen jaar ook gespecialiseerd in Ruby en Ruby on Rails applicaties.

Gerbrand van Dieijen is Java Consultant bij Whitehorses en is 6 jaar professioneel werkzaam in de ICT. Hij ontwerpt en ontwikkelt backend- en frontendapplicaties met diverse frameworks, talen en omgevingen, met een voorkeur voor de JavaVM.

Waardering:
 

Reacties

Nieuwe reactie inzenden

De inhoud van dit veld is privé en zal niet openbaar worden gemaakt.

Meer informatie over formaatmogelijkheden

CAPTCHA
Deze vraag is om te testen of u een persoon bent en om spam te voorkomen
Image CAPTCHA
Enter the characters shown in the image.