Clojure Metaprogramming With Sequences

Generating methods based on the content of arrays, hashes, and other Enumerable things is a powerful metaprogramming technique in Ruby. To keep things relatively simple, let’s use an example problem from Katrina Owen’s fantastic site Exercism:

Write a program that, given an age in seconds, calculates how old someone is in terms of a given planet’s solar years.

We know the length of an Earth year in seconds and the length of every other planet’s orbital period in terms of earth years. Here is an implementation in Ruby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SpaceAge
  attr_reader :seconds

  SECONDS_PER_EARTH_YEAR = 31557600.0

  ORBITAL_PERIODS = {mercury:  0.2408467,
                     venus:    0.61519726,
                     mars:     1.8808158,
                     jupiter:  11.862615,
                     saturn:   29.447498,
                     uranus:   84.016846,
                     neptune:  164.79132}

  def initialize(seconds)
    @seconds = seconds
  end

  def on_earth
    seconds / SECONDS_PER_EARTH_YEAR
  end

  ORBIT_PERIODS.each do |planet, earth_years_per_orbit|
    define_method "on_#{planet}".to_sym do
      on_earth / earth_years_per_orbit
    end
  end
end

We use metaprogramming in lines 22 through 26 to generate methods for ages on every planet other than Earth based on our ORBITAL_PERIODS hash. This will make it super easy to change this class when we are done with Earth and want to define everything in terms of Martian years.

Writing the Clojure equivalent of this implementation proved a bit more difficult than expected. Let’s set up the Clojure equivalent and work through the metaprogramming piece:

1
2
3
4
5
6
7
8
9
10
11
12
13
(ns space-age)

(defn on-earth [seconds]
  (/ seconds 31557600.0))

(def orbital-periods
  {:mercury 0.2408467
   :venus   0.61519726
   :mars    1.8808158
   :jupiter 11.862615
   :saturn  29.447498
   :uranus  84.016846
   :neptune 164.79132})

How might we generate functions from our orbinal-periods hashmap? A list comprehension with for feels pretty close to the mark, but this cannot work because it yields a lazy sequence. We need to execute the contents of this sequence to get the functions we are creating into our namespace. Clojure’s’ doseq macro is purpose built for this use case. Now we have the start of our solution:

1
2
3
(doseq [[planet period] orbital-periods]
  ;somehow make functions
  )

I had a bit of trouble wrapping my mind around this part of the problem because I taught myself Clojure with resources placing a heavy emphasis on lazy evaluation and side-effect free functions. This case runs totally counter to that, executing a sequnce specifically for its side effects, which happen to be producing pure functions.

Now that we know something will execute, we must determine what to execute to generate a function from a key in the orbital-periods hashmap. My first instict was to try something like this:

1
(defn (string "on-" planet) [seconds] ;do stuff)

This fails because the first argument to def or defn must be a symbol at readtime. intern solves this problem by finding or creating a var by the supplied symbol at runtime. From there, it is as easy as building the function we want bound to that var:

1
2
3
4
(doseq [[planet period] orbital-periods]
  (let [fn-name (symbol (str "on-" (name planet)))]
    (intern *ns* fn-name
      (fn [seconds] (/ (on-earth seconds) period)))))