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
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
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
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:
This fails because the first argument to
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