Homoiconicity & Feature Flags

At work we've been using feature flags to roll out various changes of the product. Most recently the rebrand from Icebreaker to Gatheround. This allowed us to continuously ship small pieces and review and improve these on their own pace without creating two vastly different branches of changes.

With the rebrand work in particular there were lots of places where we needed relatively small, local differentiations between the old and the new appearance. Oftentimes just applying a different set of classes to a DOM element. Less often, up to swapping entire components.

Overall this approach seemed to work really well and we shipped the rebrand without significant delays and at a level of quality that made everyone happy. What we're left with now is some 250+ conditionals involving our use-new-brand? feature flag.

This tells the story of how we got rid of those.

Introducing Homoiconicity

If you're well familiar with homoiconicity this may not be entirely new but for those who aren't: homoiconicity is the fancy word for when you can read your program as data. Among many other lisp/scheme languages Clojure is homoiconic:

(doseq [n (range 10)]
  (println n))

The program above can be run but it can also be read as multiple nested lists:

[doseq     [n [range 10]]    [println n]]

Now, if you know what I'm talking about you will see that I skipped over a small detail here, namely that the code above uses two types of parenthesis and that information got lost in this simplified array representation.

When doing it right (by differentiating between the two types of lists) we would end up with exactly the same representation as in the first code sample. And that is homoiconicity.

Homoiconicity & Feature Flags

With this basic understanding of homoiconicity, lets take a look at what those feature flags looked like in practice:

[:div
 {:class (if (config/use-new-brand?)
           "bg-new-brand typo-body"
           "bg-old-brand typo-large")}]
(when (config/use-new-brand?)
  (icon/Icon {:name "conversation-color"
              :class "prxxs h3"}))

And so on. Now we have 250+ of those in our codebase but don't really plan on reversing that change any time soon... so we got to get rid of them. Fortunately Clojure is homoiconic and doing this is possible in a fashion that really tickles my brain in a nice way.

Code Rewriting

... isn't new of course, CircleCI famously rewrote 14.000 lines of test code to use a new testing framework. I'm sure many others have done similar stuff and this general idea also isn't limited to Clojure. Code rewriting tools exist in many language ecosystems. But how easily you can do it in Clojure felt very empowering.

The next two sections will be about some 30 lines of code that got us there about 90% of the way.

Babashka + rewrite-clj

Babashka is a "fast, native Clojure scripting runtime". With Babashka you can work with the filesystem with shell-like abstractions, make http requests and much more. You can't use every Clojure library from Babashka but many useful ones are included right out of the box.

One of the libraries that is included is rewrite-clj. And, you guessed it, rewrite-clj helps you 🥁 ... rewrite Clojure/Script code.

I hadn't used rewrite-clj before much am still a bit unfamiliar with it's API but after asking some questions on Slack @borkdude (who also created Babashka) helped me out with an example of transforming conditionals that I then adapted for my specific situation.

I will not go into the code in detail here but if you're interested, I recorded a short 4 minute video explaining it at a surface level and demonstrating my workflow.

The rewriting logic showed in the video ignores many edge cases and isn't an attempt at an holistic tool to remove dead code branches but in our case this basic tool removed about 95% of the feature flag usages, leaving a mere 12 cases behind that used things like cond-> or conjunctions.

Of the more than 230 feature flags that have been removed only about ten needed additional adjustments for indentation. This happened mostly when a feature-flag-using conditional wrapped multiple lines of code. Due to the locality of our changes that (fortunately) was relatively uncommon. If we had set up an automatic formatter for our code this also wouldn't have required any extra work.

Onward

This has been an extremely satisfying project, if you can even call those 30 lines a "project". I hope you also learned something or found it helpful in other ways!

Thanks to Michiel "borkdude" Borkent for all his work on Babashka. The interactive development workflow shown in the video paired with blazing startup times and a rich ecosystem makes it feel like there is a lot of potential still to be uncovered.

I'd also like to thank Lee Read, who has done such an amazing job making rewrite-clj ready for more platforms like ClojureScript and Babashka as well as making sure it's future-proof by adding more tests and fixing many long standing bugs.

After writing this blog post and detailing the beginnings of this idea I also took a bit more time to clean up the code and put it on GitHub.

If you thought this was interesting, consider following me on Twitter!

Other Posts

  1. Localizing a Ghost ThemeMay 2021
  2. Clojure Macros: Creating vars from a mapFebruary 2021
  3. Promises in a ClojureScript REPLMay 2020
  4. Improved Support for Foreign Libs in cljdocMay 2020
  5. Static Blogging, Some Lessons LearnedMay 2020
  6. Working with Firebase Documents in ClojureScriptSeptember 2019
  7. 4 Small Steps Towards Awesome Clojure DocstringsJanuary 2019
  8. Sustainable Open Source: Current EffortsJanuary 2018
  9. Maven SnapshotsJune 2017
  10. Requiring Closure NamespacesMay 2017
  11. Simple Debouncing in ClojureScriptApril 2017
  12. Making Remote WorkMarch 2017
  13. Just-in-Time Script Loading With React And ClojureScriptNovember 2016
  14. Props, Children & Component Lifecycle in ReagentMay 2016
  15. Om/Next Reading ListNovember 2015
  16. Parameterizing ClojureScript BuildsAugust 2015
  17. ClojureBridge BerlinJuly 2015
  18. Managing Local and Project-wide Development Parameters in LeiningenJune 2015
  19. Formal Methods at AmazonApril 2015
  20. (lisp keymap)February 2015
  21. CLJSJS - Use Javascript Libraries in Clojurescript With EaseJanuary 2015
  22. Why Boot is Relevant For The Clojure EcosystemNovember 2014
  23. S3-Beam — Direct Upload to S3 with Clojure & ClojurescriptOctober 2014
  24. Patalyze — An Experiment Exploring Publicly Available Patent DataOctober 2014
  25. Running a Clojure Uberjar inside DockerSeptember 2014
  26. Using core.async and Transducers to upload files from the browser to S3September 2014
  27. Emacs & VimJuly 2014
  28. Heroku-like Deployment With Dokku And DigitalOceanMarch 2014
  29. Woodworking MasterclassesFebruary 2014
  30. Early Adopters And Inverted Social ProofFebruary 2014
  31. Living SmallFebruary 2014
  32. Sending You a TelegramJanuary 2014
  33. Running a Marathon, Or NotJanuary 2014
  34. Code SimplicityJanuary 2014
  35. What do we need to know?December 2013
  36. Sculley's DiseaseDecember 2013
  37. A Resurrection PostDecember 2013
  38. A Trip To The USSeptember 2013
  39. Analytics DataApril 2013
  40. Asynchronous CommunicationApril 2013
  41. From Zero to Marathon in Six MonthtsMarch 2013
  42. Git Information in Fish Shell’s PromptDecember 2012
  43. When We Build StuffAugust 2012
  44. Models, Operations, Views and EventsJuly 2012
  45. The Twelve Factor AppJune 2012
  46. Paris And BackMay 2012
  47. A Friend Is Looking For A Summer InternshipMay 2012
  48. Kandan Team ChatMay 2012
  49. Entypo Icon SetMarch 2012
  50. Startups, This Is How Design WorksMarch 2012
  51. Hosting A Static Site On Amazon S3February 2012
  52. Exim4 Fix Wrongly Decoded Mail SubjectJanuary 2012