Wednesday, April 24, 2013

Literate programming: edit a single file

My idea of proper literate programming is that I write code as I write a novel. It's a single file that I write mostly in a linear way. When I make jumps, I warn the reader that I'll make a jump. So the whole code can be read from cover to cover, so to speak. Reading the code follows my way of thinking, how I changed my mind, how I added code to implement new requirements, how I fixed bugs, etc. The following snippet shows two example sessions of such a code editing. There is no tool yet to produce proper, working source code from it.

; I just want a simple hello-world function.  (Note the ! in next
; line: it shows it is an editing command, not part of the final code)
;! (open "src/hello/core.clj")
; The file is opened.  If the file or its parent directories don't
; exist, they are created.
(ns hello.core)

(defn greet
  "Just return a greeting"
  []
  "Hello")

; It's time now to test what we've just written
;! (open "test/hello/core_test.clj")
(ns hello.core-test
  (:use
   [clojure.test :only [deftest is]])
  (:require
   [hello.core :as core]))

(deftest greeting-test
  (is
   (= "Hello" (core/greet))))

; That's it.  We are done, we can commit our code...
;----------------------------------------------------
; The customer wants our greeting to be able to accept a `fellow`
; argument to whom the greeting is addressed.  So we have to add a
; test case for that.  Since code is data, we just have to refactor
; it.  It's usually done in an editor without much thought beforehand.
; You may use some support from the editor, like `paredit`.  But I
; want to do differently this time.  I'm coding yet, just thinking
; about what I would do in the editor.  This thinking happens to be
; some code too.

; Open the file with the greeting test (it already exists now).
;! (open "test/hello/core_test.clj")
; Find the test.
;! (find 'deftest 'greeting-test)
; Go to the first assertion
;! (zoom 'is)
; We want to append a new test case at the same level
;! (up)
;! (append-next-block)
(is
 (= "Hello, Mary" (core/greet "Mary")))
; You can guess what the final code looks now.

; We can do something more difficult now, change the greeting function
; itself.  We want to have both a 0, and a 1 argument version.
; Now you know how to navigate where we want to change the code.
;! (open "src/hello/core.clj")
;! (find 'defn 'greet)
;! (zoom '[])
; We want to make this: [] "Hello" => ([] "Hello")
;! (wrap-round)
; It just wraps the arg vector.  We have extend the paren to
; include the "Hello" part.
;! (slurp-forward)
; We are done with the 0 argument version.  We just have to add
; code for 1 one argument.
;! (append-next-block)
([fellow]
   (str "Hello, " fellow))

; This modification is done, we can commit the changes.

And this is how the files would look now:

(ns hello.core)

(defn greet
  "Just return a greeting"
  ([]
    "Hello")
  ([fellow]
     (str "Hello, " fellow)))

(ns hello.core-test)

(deftest greeting-test
  (is
   (= "Hello" (core/greet)))
  (is
   (= "Hello, Mary" (core/greet "Mary"))))

No comments:

Post a Comment