{"id":17245448,"url":"https://github.com/bobbicodes/representer-blog-post","last_synced_at":"2026-01-06T22:07:40.195Z","repository":{"id":101319863,"uuid":"579282711","full_name":"bobbicodes/representer-blog-post","owner":"bobbicodes","description":"WIP article demonstrating the Exercism Clojure Representer","archived":false,"fork":false,"pushed_at":"2022-12-17T19:15:46.000Z","size":15,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-31T06:42:36.223Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bobbicodes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-17T06:57:02.000Z","updated_at":"2022-12-17T06:57:02.000Z","dependencies_parsed_at":"2024-01-20T07:47:11.179Z","dependency_job_id":null,"html_url":"https://github.com/bobbicodes/representer-blog-post","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbicodes%2Frepresenter-blog-post","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbicodes%2Frepresenter-blog-post/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbicodes%2Frepresenter-blog-post/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bobbicodes%2Frepresenter-blog-post/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bobbicodes","download_url":"https://codeload.github.com/bobbicodes/representer-blog-post/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245591625,"owners_count":20640692,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T06:29:36.666Z","updated_at":"2026-01-06T22:07:40.143Z","avatar_url":"https://github.com/bobbicodes.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# The Exercism Clojure Representer\n\nWhat is a representer? You can read about them in [this article](https://exercism.org/blog/introducing-representers), or just watch Jeremy explain it in this video:\n\n[![Representer video](https://img.youtube.com/vi/OJqN9adA_6Y/0.jpg)](https://www.youtube.com/watch?v=OJqN9adA_6Y)\n\n\n\nThe idea is very simple: We receive hundreds of exercise submissions each week, many of which are quite similar. In order to provide the most valuable feedback we can from our mentors, we have a piece of software that analyzes the solutions and identifies *common approaches* used. It does this by normalizing for various things, namely whitespace, macroexpansion, and variable names.\n\n## Clojure Representer implementation\n\nMost 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 use a local version that is modified slightly so that instead of making the names *unique*, it makes them *generic*. This normalizes the solutions for variable names, replacing each occurrance with a placeholder name.\n\nIn a future post we will 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.\n\n## Common approaches to the `two-fer` exercise\n\nI ran the last 500 submissions to `two-fer` through the representer and processed them into a map using this function:\n\n``` clojure\n(defn representations [exercise]\n  (let [representations\n        (apply merge\n               (for [n (solution-dirs exercise)]\n                 {n (representation exercise n)}))\n        repmap-seq      (for [n (solution-dirs exercise)]\n                          {:solution       n\n                           :code           (solution exercise n)\n                           :representation (get representations n)\n                           :times-used     (get (frequencies (vals representations))\n                                                (representation exercise n))})]\n    (reverse (sort-by :times-used\n                      (map #(select-keys % [:code :times-used])\n                           (distinct-by :representation repmap-seq))))))\n```\n\nThis creates a sequence of maps consisting of only the solutions resulting in unique representations, and sorts them by the number of times used, highest to lowest.\n\n### Approach 1 - 114 submissions\n\n``` clojure\n(defn two-fer \n  ([] (two-fer \"you\")) \n  ([name] (str \"One for \" name \", one for me.\")))\n```\n\nIt's a typical multi-arity function, where the nullary arity calls the unary arity supplying the default argument.\n\n### Approach 2 - 79 submissions\n\n``` clojure\n(defn two-fer \n  ([] \"One for you, one for me.\") \n  ([name] (str \"One for \" name \", one for me.\")))\n```\n\nHere instead of calling itself, the default result is supplied as its own string.\n\n### Approach 3 - 37 submissions\n\n``` clojure\n(defn two-fer \n  ([] (two-fer \"you\")) \n  ([name] (format \"One for %s, one for me.\" name)))\n```\n\nIt is like the first approach, but the variable is interpolated using `format` instead of concatenated with `str`.\n\n### Approach 4 - 29 submissions\n\n``` clojure\n(defn two-fer \n  ([] (str \"One for you, one for me.\")) \n  ([name] (str \"One for \" name \", one for me.\")))\n```\n\nLol, they're concatenating a single string... since this is functionally identical to the second approach, so it might be worth adding this to the representer as a rule.\n\n### Approach 5 - 25 submissions\n\n``` clojure\n(defn two-fer \n  ([] \"One for you, one for me.\") \n  ([name] (format \"One for %s, one for me.\" name)))\n```\n\nThis is like the second approach, but using `format` instead of `str`.\n\n### Approach 6 - 24 submissions\n\n``` clojure\n(defn two-fer \n  ([name] (str \"One for \" name \", one for me.\")) \n  ([] (two-fer \"you\")))\n```\n\nThis is like the first approach, but with the arities switched! This is another way we could improve the representer, by having it sort them.\n\n### Approach 7 - 23 submissions\n\n``` clojure\n(defn two-fer \n  ([name] (str \"One for \" name \", one for me.\")) \n  ([] \"One for you, one for me.\"))\n```\n\nSee what's going on here?\n\n### Approach 8 - 11 submissions\n\n``` clojure\n(defn two-fer \n  ([name] (str \"One for \" name \", one for me.\")) \n  ([] (str \"One for you, one for me.\")))\n```\n\nYep. It's like permutations of ice cream flavors!\n\n### Approach 9 - 8 submissions\n\n``` clojure\n(defn two-fer \n  ([name] (format \"One for %s, one for me.\" name)) \n  ([] \"One for you, one for me.\"))\n```\n\nBuild string with `format`; arities reversed\n\n### Approach 10 - 5 submissions\n\n``` clojure\n(defn- _two-fer [name] \n  (str \"One for \" name \", one for me.\"))\n   \n(defn two-fer \n  ([] (_two-fer \"you\")) \n  ([name] (_two-fer name)))\n```\n\nThis is a new one - we have 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.\n\n### Approach 11 - 4 submissions\n\n``` clojure\n(defn two-fer [\u0026 [name]] \n  (format \"One for %s, one for me.\" (or name \"you\")))\n```\n\nThis one uses a variadic function, taking a variable number of arguments instead of writing a separate function body for each arity. It uses `or` to return `name` is it is non-nil, otherwise \"you\".\n\nApproach 12 - 3 submissions\n\n``` clojure\n(defn two-fer \n  ([name] (format \"One for %s, one for me.\" name)) \n  ([] (two-fer \"you\")))\n```\n\nFormat, reversed function bodies, one arity calls the other.\n\n### Approach 13 - 3 submissions\n\n``` clojure\n(defn two-fer [\u0026 name] \n  (str \"One for \" (or (first name) \"you\") \", one for me.\"))\n```\n\nLike the variadic solution above, but building it with `str` instead of `format`, and the argument list is slightly different in that it isn't destructured.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbobbicodes%2Frepresenter-blog-post","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbobbicodes%2Frepresenter-blog-post","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbobbicodes%2Frepresenter-blog-post/lists"}