Architecture enabling business

A domain-specific language for … interval workouts

Posted in cycling, dsl by jeromyc on January 26, 2009

This is something of a cross-over personal/technology post, hopefully interesting nonetheless. Perhaps a something-for-everyone post … or a nothing-for-anyone post.

I’m a fairly serious amateur cyclist. I raced for one season when I was in college (1992!), but I totally burned out and quit riding, almost entirely, until the 2007 season. I was helped back into riding through what I call “motivation through financial commitment” – I bought a $5000 (at the time) Cervelo Soloist Carbon. But my biggest motivation comes from the group I ride with – Crack o’ Dawn – that meets every weekday at 5:45 for a 25-30 mile ride and does weekend rides of 50-100 miles; the group convenes within 2 miles of my house in Newton, MA. I rode 5000ish miles in 2007 and 6500ish in 2008 (the CoD record this year was set by Michael Mercier, at 14105.69 miles, and many CoDers break 10K every year).

Now the bad news. It’s winter here in the Boston area. It’s a snowy winter. Unlike some of my CoD mates, I’m not all that fond of the cold or rolling the dice with icy roads (I had a crash late in the 2007 season, broken collarbone). I’m happy to ride if it’s above freezing, even if wet, and down to 20ish if it’s dry. But lately this has left precious little opportunity to ride outside. I’m coping by riding spin classes at Spynergy, where follow CoDer and good friend Andy Schiller is an awesome instructor. I also have a CycleOps Pro 300PT indoor trainer, which I ride 4-6 times a week. The coolest thing about about the trainer is that it has an integrated power meter, so you can track your wattage while you ride. There’s a lot of science behind training based on power; the Peaks Coaching Group is pretty well known, and their principal, Hunter Allen, is co-author of book on the topic: Training and Racing with a Power Meter (Allen and Coggan). This provides an excellent opportunity for me to integrate my bike self and my geek self into one well-trained, intellectually satisfied unit.

Here’s how: unlike some indoor training systems, like the CompuTrainer, which many of my cohorts swear by, the CycleOps doesn’t provide any facilities for guiding a workout. It just shows you your heart rate, cadence and power output in watts. I filled this gap with a small Windows Forms application (hopefully soon to be ported to WPF) that has a very simple UI (yes, some would say go so far as “lame”, and they’d be right, but that’s not the point):

picture-2This says: ride at 200 watts for 4 minutes and 44 seconds more; you’ve been riding for 19 seconds and have 54:44 to go. The pull down lets you select different workouts. You can skip over an interval (wimp) and pause/resume (say, to change the TV channel). Easy, to the point. But, there’s some underlying extra fun. When I first wrote this, I basically hardcoded workouts in a config file. They looked like this:

        180     00:10:00
        220     00:10:00
        250     00:05:00
        280     00:02:00
        150     00:03:00
        220     00:10:00
        250     00:05:00
        280     00:02:00
        150     00:03:00

Again, simple and to the point. But repetitive and not very flexible. The opportunity expands…

I’ve been working with domain-specific languages in various forms for a long time, including with Microsoft’s DSL Tools, Boo and Oslo. As I’ve mentioned before, my good friend Harry Pierson is a Product Manager with the DLR group at Microsoft, so I’ve been playing with IronRuby and IronPython for a while (Harry gave an excellent talk at a brown bag session at VistaPrint on the DLR and IronPython a couple of months ago). I had read a few articles about implementing DSLs in Ruby, so … <light bulb/> Construct a Ruby DSL to express interval workouts and dispatch out from the WinForms app to the IronRuby runtime to fetch workout definitions.

Here’s a simple example of a definition in the resulting DSL:
        workout "W - intervals" do
        interval.target(0).time('00:01:00')
        interval.target(180).time('00:05:00')
        repeat 4 do
              interval.target(220).time('00:10:00')
              interval.target(280).time('00:02:00')
              interval.target(150).time('00:03:00')
        end
end

This defines a workout called “W – intervals”, made up of a 1 minute 0-power “click start, get on the bike and clipped in” step, 5 minutes at 180 watts, then 4 repeats of 10 minutes at 220, 2 at 280 (what I believe to be my functional threshold power) and 3 at 150. Aside: I’m not proposing this is a good workout – I’m studying the Allen and Coggan book to figure out how to actually use power properly in training. Again, not the point.

Simple enough, and the “repeat” construct achieves a lot of the intended benefit of reducing repetition. But there’s so much more potential. As I was reading the training book today, I came up with this one:

ftp = 280
work = 0.9*ftp
warmup = 0.68*ftp
rest = warmup

workout "L4 - Ride A" do
        interval.target(0).time('00:01:00')
        interval.target(warmup).time('00:05:00')
        interval.target(ftp).time('00:02:30')

        repeat 2 do
                interval.target(work).time('00:13:00')
                interval.target(rest).time('00:02:00')
        end

        repeat 6 do
                interval.target(work).time('00:01:00').message("high cadence")
                interval.target(rest).time('00:02:00')
        end

        interval.target(rest).time('00:05:00')
end

This is an adaptation (shortening to 1 hour) of a “typical level 4 threshold” workout from Coggan and Allen, on page 83.

The power of using an embedded DSL is clear: there are very few domain-specific implementation constructs required, making the DSL specification extremely short. All that was required was the definition of the workout and interval classes and the simple “repeat” construct, and that’s really only a personal preference (6.times would have done just as well).

The core of the Ruby DSL looks like this:

$intervals = []

class RInterval
        def initialize()
                @interval = Interval.new
                $intervals.push self
        end

        def target(t)
                @interval.Target = t
                self
        end

        // message and time look the same as target

        def Interval
                @interval
        end
end

def repeat(count, &block)
        count.times do
                yield
        end
end

def workout(name, &block)
        yield

        intervals = System::Collections::ArrayList.new

        $intervals.each {|interval| intervals.Add interval.Interval}

        $intervals.clear

        workouts.Add name, intervals
end

def interval
        RInterval.new
end

On the .NET side, there’s a simple Interval class:

public class Interval
{
        public TimeSpan Time { get; set; }
        public int Target { get; set; }
        public string Message { get; set; }
}

At runtime, the .NET application instantiates the IronRuby engine, sets up a Dictionary<string, ArrayList> in the engine’s ScriptScope (no interoperable generics right now :-( ) and loads and executes the DSL spec and the DSL instance:

            _scope = _engine.CreateScope();
            _scope.SetVariable("workouts", new Dictionary<string, ArrayList>());

            // ...

            _engine.Execute(code, _scope); // code is the DSL definition + the DSL instance
            object ret = _scope.GetVariable(returnVar);

And that’s it.  The UI code is dead simple, and again, not the point.  I’ve obviously got some more to learn about doing DSLs in Ruby, but this has proven to me the utility of the approach.  And it helps with the winter riding blues.

Update: Here’s the code from Jim’s comment, which I couldn’t make show up in his comment:

;; User setting
(setq *ftp_limit 280)

;; Workout routine
(progn
  (wait 1 "click start, get on the bike and clipped in")
  (warmup 5)
  (ftp 2.5)
  (repeat 2
                (work 13)
                (relax 2))
  (repeat 6
                (work 1 "high cadence")
                (relax 2))
  (relax 5))

;; Here’s the DSL implementation

(defmacro interval (pace duration &optional message)
  `(progn
                 (format t "~&~6a (~2$) for ~5 minutes" (car ,pace) (cadr ,pace) ,duration)
                 (and ,message (format t " *** ~a ***" ,message))))

(defmacro proportional-interval (name proportion duration &optional message)
  `(interval (list ',name (* ,proportion *ftp_limit*)) ,duration ,message))

(defmacro deffactor (name factor)
  `(defmacro ,name (duration &rest args)
                 `(proportional-interval ,',name ,',factor ,duration ,@args)))

(defmacro repeat (times &body body)
    (let ((x (gensym)))
      `(dotimes (,x ,times)
         ,@body)))

;; You can argue about whether the following is in the DSL designer
;; land, or in user land. Since it seems effort ratios would be
;; uniform across many workouts, I’ve put it here (though users could
;; override for their own workout easily enough.)

(deffactor ftp           1)
(deffactor work          0.9)
(deffactor relax         0.68 )
(deffactor warmup        0.68 )
(deffactor wait          0)
(defvar    *ftp_limit* 280)

Update: I couldn’t leave this alone, after Jim’s elegance put me to shame. Here’s the same workout I had before in my new DSL:

workout "L4 - Ride A" do
	setup 1
	warmup 5
	work 2

	2.times do
		work 13
		rest 2
	end

	6.times do
		work 1, "high cadence"
		rest 2
	end

	rest 5
end

Not quite ready to share the DSL definition yet – it’s still a bit ugly, but improving. I’m also trying to smooth out the IronRuby integration so that I can test the DSL independent of a .NET runtime.

Update: Here’s the new DSL definition.  Much much cleaner.  Thanks, Jim, for driving me to get this right.


$ftp = 280
$effort = {"setup" => 0, "ftp" => $ftp, "easy" => 0.8*$ftp, "work" => 0.9*$ftp, "warmup" => 0.68*$ftp, "rest" => 0.68*$ftp}

class WorkoutDSL
    def method_missing(sym, *args)
          interval = {:target => $effort["#{sym}"], :time => args[0], :message => args[1]}
          @workouts[@name].push interval
    end

    def workout(name, &block)
          @name = name
          @workouts[@name] = []

        yield
    end

    def load(filename)
          @workouts = {}
           instance_eval(File.read(filename), filename)
        @workouts
  end
end

WorkoutDSL.new.load("intervals.dsl")

2 Responses

Subscribe to comments with RSS.

  1. sokoloff said, on January 26, 2009 at 11:49 pm

    When I think of the utility of a DSL, a significant measure is succinctness. (Unfortunately, the example above looks like it’s calling pre-packaged subroutines in an API rather than a true DSL.) If you ignore the parens, the following feels a lot more DSL-y to me:

    [Jeromy: I couldn't get Jim's code to show up here, so I embedded in an update to the post.]

  2. jeromyc said, on January 27, 2009 at 12:23 am

    Yep, Lisp is a really nice language for building DSLs, based on what I’ve read/seen. But now I’ve got some nice ideas to improve my version – there are a lot of potential improvements… stay tuned.


Leave a Reply

You must be logged in to post a comment.