A 2006 article by Jeff Atwood titled Code Tells You How, Comments Tell You Why showed up on reddit/r/programming today.
It makes a good point. However, it got me thinking that for cases like the binary-search example in the article, it might be nice to see all of the alternatives in the code and easily be able to switch between them.
One way to accomplish this in Lisp is to abuse the #+
and #-
reader macros:
#+i-wanted-to-do-this
(loop :for i :to n :summing (* i i))
#+my-first-attempt-was-something-like-this
(do ((i 0 (1+ i))
(sum 0 (+ sum (* i i))))
((> i n) sum))
#+but-i-could-not-do-that-because
"Some people find a do-loop to hard to read
(and 'too' too hard to spell, apparently)."
#-now-i-know-better-and-can-do-this
(/ (* n (1+ n) (1+ (+ n n)) 6))
This is less than ideal for a number of reasons, including: one needs to make sure to pick “feature” names that won’t actually ever get turned on, the sense of +
and -
seem backwards here, and switching to a different alternative requires editing two places.
Another Lisp alternative is to abuse the case
form:
(case :now-i-know-better-and-can-do-this
(:i-wanted-to-do-this
(loop :for i :to n :summing (* i i)))
(:my-first-attempt-was-something-like-this
(do ((i 0 (1+ i))
(sum 0 (+ sum (* i i))))
((> i n) sum)))
(:but-i-could-not-do-that-because
"Some people find a do-loop to hard to read
(and 'too' too hard to spell, apparently).")
(:now-i-know-better-and-can-do-this
(/ (* n (1+ n) (1+ (+ n n)) 6)))))
This is better. No one can doubt which alternative is in use. It is only one edit to switch which alternative is used. It still feels pretty hackish to me though.
One can clean it up a bit with some macrology.
(flet ((symbol-is-***-p (sym)
(and (symbolp sym)
(string= (symbol-name sym) "***")))
(final-clause-p (clause)
(when (listp clause)
(destructuring-bind (tag &body body) clause
(when (and (symbolp tag)
(member (symbol-name tag)
'("***" "FINAL" "BLESSED")
:test #'string=))
body)))))
(anaphora:acond
((member-if #'symbol-is-***-p clauses)
(let ((clause (first (rest anaphora:it))))
`(progn
,@(rest clause))))
((find-if #'final-clause-p clauses)
`(progn
,@(rest anaphora:it)))
((last clauses)
`(prog
,@(rest (first anaphora:it)))))))
With this macro, one can now rewrite the sum-i^2
function quite readably:
(alternatives
(i-wanted-to-do-this
(loop :for i :to n :summing (* i i)))
(my-first-attempt-was-something-like-this
(do ((i 0 (1+ i))
(sum 0 (+ sum (* i i))))
((> i n) sum)))
(but-i-could-not-do-that-because
"Some people find a do-loop to hard to read
(and 'too' too hard to spell, apparently).")
(now-i-know-better-and-can-do-this
(/ (* n (1+ n) (1+ (+ n n)) 6)))))
If I wanted to try the my-first-attempt-was-something-like-this
clause, I could stick a ***
before that clause or change its name to ***
or final
or blessed
, or I could move that clause into the last spot.
There is still an onus on the developer to chose useful alternative names. In most production code, one wants to clean out all of the dead code. On the other hand, during development or for more interactive code bodies, one might prefer to be able to see the exact “How” that goes with the “Why” and easily be able to swap between them.
(Above macro coming in well-documented library form, hopefully this weekend.)
I use a reader macro that combines the #+feature style “comment next form” with a line comment. I’ve grown to like it quite a bit.
It grew out of my frustration reading code that used #+(or) to comment forms without noting why they were commented.
Here’s an example:
(defun sum-to-n (n)
#; we don’t need to loop
(loop for i from 1 to n sum i)
; because the following computes the same value:
(/ (* n (+ n 1)) 2))
It is probably not as clear as your `alternatives` for `alternatives` specific use case, but I find it convenient for similar sorts of things. I’ve published it as `comment-line-suppress-forms` in the repository at https://github.com/m-n/dishes .
Imho the right tool for tracking code history is version control system. And history is nothing else than alternatives. You could keep interesting spots (alternatives) with tags or alternatively with branches.
No, I don’t think so.
You can’t keep a tag or branch for *every single thing* that you try out.
What *might* be possible (with git, mercurial, and so on) to have a *branch* for each function where you explore various ways – and then “merge” to only a single one of them.
That entails quite a bit of digging to see the reasoning for an entire call path, though.
IMHO, source control is the place to keep all of the bugs for posterity. The point here was to find an alternative to explanatory comments like this one:
algorithm for the data sets of interest, thus we have used the more
complex, but faster method even though this problem does not at
first seem amenable to a string search technique. */
Having the old versions in source control is never as useful as one would hope. I never want to start using the code from eight months ago. So much has happened since then. If I think a binary search might be better now that the datasets have morphed, without the comment above, am I even going to think to look in source control to see if someone checked in a working version of it? If I do find one, then I have to check out the tagged version from the way-back machine, copy that one function aside somewhere, check out the current version again, integrate that old version into the latest source code (which is the only one that reads the new data format and the only one that fixes that bug in the curve-fitting algorithm).
I’m not saying that I want to keep around the bug-ridden chunks of code. In most cases, these alternatives would be a maintenance headache as I found when the do-loop that I put in all three examples had the wrong terminating condition and was summing
(* n n)
instead of(* i i)
. However, I think there are some cases where one might like an easier way to explore the alternatives than having to do the source control dance.I think I’d prefer to either use strings for tags (so that there aren’t that many unused symbols out there [because of trying the alternatives] that might get autocompleted).
Otherwise, that’s a very neat idea! Please push that into some general utility library instead of a new one; there are already too many 😉
Indeed, I felt like I could take this article and warp it into tolerable documentation quickly, but I wanted to tweak the macro itself to make sure one could use strings rather than symbols.
And, now that Quicklisp is around, I actually think that tiny, separate libraries are the way to go.
“sum-i^2”, but the parameter is “n”? Either name it “sum-n^2” or change the parameter to i.
Pedantically yours…
It is the sum of i^2 as i goes from zero to n.
@Philipp Marek
This may be true for such small alternatives in one function, but: most alternatives are different algorithms implemented by several functions. Those must match each other. Furthermore, if you atr just interested in the current implementation you have to search for that, and you have to make shure to know which one is currently selected.
I sometimes like to keep alternative versions of functions around as well. For example, when I made a perlin noise implementation it was good to keep the reference implementation around (I just tagged “-reference” after the function name).
For an API it is also nice to have long descriptive function names and shorter ones to preserve the holy horizontal space. I link these using #’defalias:
https://github.com/aerique/okra/blob/master/src-common/common.lisp#L29
See usage here:
https://github.com/aerique/okra/blob/master/src/vectors.lisp#L174
Try Literate Programming with Org-Mode and get the best for each reader. No pun intended.
If it played better with SLIME, I’d be using it. I love LP.