Introducing the Exercism Clojure Representer
What is a representer? You can read about them in this article, or just watch Jeremy explain it in this video:
The problem statement is very simple: The heart of the Exercism community is all about mentorship. We have received hundreds of thousands of exercise submissions for the Clojure track alone, many of which are quite similar as I intend to highlight in this article. This results in our mentors having to repeat themselves a lot, which makes poor use of what is one of our biggest strengths - the human connection. We want to flip the situation around by allowing them to share their knowledge where it can be much more valuable - the uncommon submissions.
The strategy we have chosen to accomplish this is to develop a system of analyzing code to identify the most commonly used approaches, and building a UI that allows our most trusted mentors to submit automated comments to be displayed whenever that approach is used. This is done via the new Automation tab in the Mentoring dashboard:
This has enormous potential for maximizing the effect of their effort - instead of repeating the same advice each time, they simply provide it once.
Clojure Representer implementation
The Clojure Representer works by taking the student's solution and applying a series of normalizations, each accounting for differences that do not affect the approach used, such as whitespace, macros, and variable names.
Most of the work is done using clojure.tools.analyzer, with additional processing done with rewrite-clj. We make use of the analyzer for its macroexpansion capability, as well as its ability to emit Clojure forms from the generated AST. Specifically, we use the emit-hygienic-form
function which includes a pass called uniquify
which normally replaces all the local variables with unique identifiers, but we use a local version that is modified slightly so that instead of making the names unique, it replaces each occurrance with a generic placeholder name.
In a future post we may dive more into the implementation, but that's the basic idea. Here, what I'd like to do is just show it in action by demonstrating its practical use - grouping the solutions into common approaches.
Common approaches to the two-fer
exercise
I ran the last 500 submissions to two-fer through the representer and processed them into a sequence of maps consisting of only the solutions resulting in unique representations, and sorted them by the number of times used. The exercise is very simple, it teaches the concept of default parameters. Here are the 7 most common occurrances, which together account for 365 of them:
Approach 1 - 142 submissions
(defn two-fer
([] "One for you, one for me.")
([name] (str "One for " name ", one for me.")))
(two-fer "Alice")
xthe evaluation will appear here (soon)...
(two-fer)
the evaluation will appear here (soon)...
The most common solution (just by a hair) is a standard multi-arity function which either returns a string concatenated with the supplied argument, or a default string if none supplied.
Approach 2 - 138 submissions
(defn two-fer
([] (two-fer "you"))
([name] (str "One for " name ", one for me.")))
the evaluation will appear here (soon)...
Slightly less often used it is a solution that is very similar, except a bit of repitition is eliminated by having the first arity call the second one, passing the default parameter that way.
Approach 3 - 48 submissions
(require [goog.string.format])
(defn format
[fmt & args]
(apply goog.string/format fmt args))
(defn two-fer
([] (two-fer "you"))
([name] (format "One for %s, one for me." name)))
the evaluation will appear here (soon)...
It is like the second approach, but the variable is interpolated using format
instead of concatenated with str
.
(The format
function using goog.string.format
was removed from Clojurescript, so we need to define it for this snippet to work. This is not necessary for the Clojure solution.)
Approach 4 - 25 submissions
(defn two-fer
([] "One for you, one for me.")
([name] (format "One for %s, one for me." name)))
the evaluation will appear here (soon)...
This is like the first approach, but using format
instead of str
.
Approach 5 - 5 submissions
(defn- _two-fer [name]
(str "One for " name ", one for me."))
(defn two-fer
([] (_two-fer "you"))
([name] (_two-fer name)))
the evaluation will appear here (soon)...
This solution uses a helper function that builds the string, allowing the main function to be simplified down to a dispatch function. While this may not seem like a huge advantage here, it is a useful pattern to understand for when dealing with more complex functions later.
Approach 6 - 4 submissions
(defn two-fer [& [name]]
(format "One for %s, one for me." (or name "you")))
the evaluation will appear here (soon)...
This one uses a variadic function with destructuring, taking a variable number of arguments instead of writing a separate function body for each arity. It uses or
to return name
if it is non-nil, otherwise "you".
Approach 7 - 3 submissions
(defn two-fer [& name]
(str "One for " (or (first name) "you") ", one for me."))
the evaluation will appear here (soon)...
Like the variadic solution above, but building it with str
instead of format
, and without the destructuring.
This illustrates the value of the representer. By simply writing a bit of tooling that identifies these top 7 approaches, we can provide automated feedback for 75% of our submissions!
To support ongoing work on the Exercism Clojure track, you can become a patron. I also often livestream development on my YouTube channel. Thanks for reading!