Skip to content

Instantly share code, notes, and snippets.

@oakes
Last active January 6, 2024 07:16
Show Gist options
  • Star 68 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oakes/8db57ac808bf6ec144d627fd83a89da3 to your computer and use it in GitHub Desktop.
Save oakes/8db57ac808bf6ec144d627fd83a89da3 to your computer and use it in GitHub Desktop.

How Clojure's documentation can leapfrog other languages

Summary

I made a documentation generator that cashes in on Clojure's dynamism. See the play-cljs docs (a ClojureScript game library) for an example of its output.

The Problem

Like many of you, I've often wondered what my final regret will be on my deathbed. My best guess came to me in a dream recently. I was walking across the charred earth of an apocalyptic future world, maneuvering around the remains of the less fortunate. I was startled to find a young girl, barely holding onto her life. She murmured something to me. I asked her to repeat it, and she said more loudly: "I...wish your Clojure projects didn't have such crappy documentation."

If you take criticism the way I do, you probably had the same first thought: Let the girl die. But in truth she had a valid point. Dynamic languages have a bad reputation when it comes to documentation. On the surface, it makes sense: Documentation generators work by statically analyzing source code, and since static languages have more information available statically (duh), their docs will tend to be better. Perhaps the only winning move for dynamic languages is not to play.

Static vs. Dynamic Documentation

Try to forget this notion that documentation is primarily a static thing that library authors put on the web for users and search engines to passively consume. Instead, imagine if end users created docs on-the-fly for every dependency in their project, using their doc generator as a real-time developer tool. That is the idea behind my new project called Dynadoc.

clojure.core

Dynadoc runs inside your Clojure(Script) project, spinning up a server on localhost to show its UI. It finds all the namespaces and generates searchable docs instantly. This is possible because in Clojure, everything is available at runtime, including docstrings and source code. You can even define a new function in your project, eval it, refresh Dynadoc and see it there.

Interactive Examples

Since we're running inside your project, we can do some pretty crazy stuff. Dynadoc allows you to define code examples for anything, and it will display them in the correct page in a fully interactive way. Here's the venerable conj function with interactive examples:

demo-clj

Some people get fixated on docstrings as the source of our documentation woes, but I disagree completely. You could write an entire novel about conj and it still won't be as good as a few interactive examples. And for non-English speakers, interactive examples beat docstrings by a long shot.

To define these examples, you just need to use a special macro I made. The above examples were created thusly:

(defexamples clojure.core/conj
  ["Add a name to a vector"
   (conj ["Alice" "Bob"] "Charlie")]
  ["Add a number to a list"
   (conj '(2 3) 1)]
  ["Add a key-val pair to a hash map"
   (conj {:name "Alice"} [:age 30])])

The best part is, you don't need any buy-in from the core team or library authors. We can start writing examples now, for anything in Clojure, and they will be usable for any tool that wants to do something with them.

ClojureScript examples are even wilder. They can optionally ask for a DOM element, so the examples can do something visual:

demo-cljs

That's a Reagent component, with a completely interactive example inside its own documentation page! You can try it live. Here's the code:

(defn clicks
  "Shows the number of times the user clicked the button."
  [button-text]
  (let [state (r/atom {:clicks 0})]
    (fn []
      [:div
       [:p "You clicked " (:clicks @state) " times"]
       [:button {:on-click (fn []
                             (swap! state update :clicks inc))}
        button-text]])))

(defexample clicks
  {:with-card card
   :with-focus [focus [clicks "Click me!"]]}
  (reagent.core/unmount-component-at-node card)
  (reagent.core/render-component focus card)
  nil)

This example is a bit weirder. The :with-card option takes a symbol that will be associated with a DOM element in your example. The :with-focus option takes a binding form where you can define a small chunk of code that you want to be exclusively visible in the example. That way, the distracting setup code isn't viewable or editable. In the body of the example, we make sure to unmount any previous component and mount a new one, focus, on the DOM element, card.

Through the magic of Clojure macros, the code is rewritten and evaluated in your browser. Every time the user edits the code in the browser-based editor, the example is re-executed, so the component is unmounted/remounted with the new data. The great thing about this is that it works with absolutely anything. It gives you a DOM element and you take care of the rest. Here's an example I made for my game library, play-cljs:

demo-cljs2

Check out the full play-cljs dynadocs. The fact that they are being served on Github Pages reflects the last feature...good ol' fashioned static HTML export. Dynadoc supports it via the "Export" link. The Clojure examples won't be interactive (they need a JVM to be present), but the ClojureScript examples can be - because it can compile itself in the browser. Drops mic.

Conclusion

Clojure is dope, and if you use it your life will be dope too.

@tomisme
Copy link

tomisme commented Dec 11, 2017

Fantastic! :D

I've been using devcards in all of my recent clojurescript projects in a similarish way, just manually making cards that test library functionality. The two tools seem to have pretty different goals but also overlap a little. I wonder if it would be possible to render both kinds of 'cards' in the same interface somehow..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment