The Artima Developer Community
Sponsored Link

Ruby Code & Style
If It's Not Nailed Down, Steal It
Pattern Matching, S-Expressions, and Domain Specific Languages in Ruby
by Topher Cyll
May 23, 2006

<<  Page 4 of 5  >>

Advertisement

Theft #3: External Domain Specific Languages

DLSs, or domain specific languages, are hot. Sometimes a general-purpose programming language just isn’t the clearest or shortest way to solve some problem. Creating a sub-language that can solve the problem more elegantly is one of the best ways to get around this.

Martin Fowler, an advocate of object-oriented programming, patterns, and agile software development, divides DSLs into two types. In his own words [1], “External DSLs are written in a different language than the main (host) language of the application and are transformed into it using some form of compiler or interpreter… Internal DSLs morph the host language into a DSL itself.”

Internal DSLs

Most Ruby DSLs are Internal DSLs. Here’s an example from the Dwemthy’s Array DSL in the infamous and terrifying Chapter 6 of Why’s Poignant Guide to Ruby2:

  class TeethDeer < Creature
     life 655
     strength 192
     charisma 19
     weapon 109
  end

This code describes the fearsome TeethDeer creature, known for it’s deadly bite. There’s no magic here, just some class methods. It’s nice that this code is only Ruby. Ruby on Rails also uses some small Internal DSLs in Ruby. David Heinemeier Hansson extends some of the core Ruby classes to let you write 3.days.ago and have it do the right thing. Unfortunately, coding these kinds of DSLs can get complex, and the Ruby syntax may ultimately feel limiting.

External DSLs

External DSLs aren’t as common in Ruby, but they do show up. For example, the Ruby DBI library uses embedded SQL:

  sth = dbh.prepare("SELECT * FROM users");

So, as a point of comparison, 3.days.ago is still all Ruby code, but “SELECT * FROM users” is not.

Logo

Okay, enough talk, let’s implement a simple version of the Logo programming language in Ruby. Remember Logo3? That language with the silly turtle that draws? Logo code is very simple and traditionally looks like this:

  repeat 4 [ forward 100 right 90 ]

This produces:

Logo output 1

Our logo will differ only in that instead of square brackets for blocks, we’ll use parenthesis:

  repeat 4 ( forward 100 right 90 )

The “good old days” of just flipping bits in video memory are long gone, so let’s draw SVG (scalable vector graphics) images instead. SVG is an XML drawing format, and since it’s just text, it should be relatively easy for us to generate. We’ll start by creating a Logo class, and giving it a render() method

  def render
    return <<-END
  <?xml version="1.0" standalone="no"?>
  <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" 
  "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
  <svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
   <g stroke="black" stroke-width="1">
    #{@buffer}</g>
   </svg>
  END
  end

This is an alarming snippet, but it just creates the headers and root element destined for our SVG file, and dumps the instance variable @buffer as part of the output. The real trick is going to be turning the Logo commands into SVG lines and storing them in that buffer.

The Turtle

Just to refresh your memory, the little Logo turtle drags a pen with him, so every time he moves, he draws a line. The most basic commands he listens to are ‘left’, ‘right’, and ‘forward’. Left and right ask him to turn some number of degrees left or right, and forward tells him to walk some number of pixels forward.

Therefore, we’ll have three other instance variables: @x, @y, and @angle. In order to keep @angle between 0 and 360 (not strictly necessary, but it’s cleaner this way), let’s make a setter for angle.

  def turn(num)
    @angle = (@angle + num) % 360
  end

Now, when the turtle moves forward, we’ve got to actually draw a line. Using the standard polar to cartesian conversion, the move() method computes the turtle’s new position and draws a line between there and his old position (so long as the @pen instance variable is set). Of course, he doesn’t actually draw the line, he just puts the text representing it into our @buffer.

  def move(distance)
    oldx, oldy = @x, @y
    radians = @angle * Math::PI / 180
    @x += distance * Math.cos(radians)
    @y += distance * Math.sin(radians)
    return unless @pen
    @buffer += <<-END
  <line x1="#{oldx}" y1="#{oldy}" x2="#{@x}" y2="#{@y}"/>
    END
  end

We’d have enough now to actually render something if we had an initialize() method to setup @x, @y, @angle, and @pen. Then we could write:

  logo = Logo.new
  logo.move(100)
  logo.turn(90)
  logo.move(100)

That’s just Ruby though, so let’s add an eval method. Note that we wrap the string in parenthesis, so that the ‘sexp’ library will return us a list of all the commands.

  def eval(string)
    run("(#{string})".parse_sexp)
  end

<<  Page 4 of 5  >>


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2017 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us