views:

195

answers:

4

I've been learning scheme, and I just realized that I don't really know how to properly comment my functional scheme code. I know how to add a comment of course - you add a ; and put your comment after it. My question is what should I put in my comments, and where should I comment for maximum readability and comprehensability for other programmers reading my code?

Here's a code snippet I wrote. It's a function called display-n. It can be called with any number of arguments and outputs each argument to the screen in the order that they are provided.

(define display-n
  (lambda nums
    (letrec ((display-n-inner 
              (lambda (nums)
                (display (car nums))
                (if (not (equal? (cdr nums) (quote ()))
                    (display-n-inner (cdr nums))))))
      (display-n-inner nums))))

Edit: Improved tabbing and replaced '() with (quote ()) to avoid SO messing up the formatting.

I'm just not sure how/where to add comments to make it more understandable. Some scheme code I've seen just has comments at the top, which is great if you want to use the code, but not helpful if you want to understand/modify it.

Also - how should I comment macros?

A: 

I think a great place to start would be to put your one-sentence description of what the function does

It can be called with any number of arguments and outputs each argument to the screen in the order that they are provided.

as a comment at the beginning.

I'm not particularly conversant in scheme, so I can't comment (:-) on whether additional line-by-line comments explaining the mechanics of how the function achieves that result would be expected according to normal scheme style (but I suspect not).

David Gelhar
+1  A: 

I follow an approach similar to what's posted here:

http://www.cc.gatech.edu/computing/classes/cs2360/ghall/style/commenting.html

Note: this is for Common Lisp.

Specifically:

" Four Semicolons(;;;;)
...denote a sub heading in the file...

Three Semicolons(;;;)
...denote a description of the succeeding function, macro, or
variable definition...
[I usually just most of the description into the "docstring"
  of the function or variable.] 


 Two Semicolons(;;)
 ...denote a description of the succeeding expression...

 One Semicolon(;)
 ...denotes an in-line comment that explains a particular element
    of the expression on that line... Brevity is important for
    inline comments"
chollida
+4  A: 

Some random notes:

  • Traditionally, Scheme and Lisp code has used ;;; for toplevel comments, ;; for comments in the code, and ; for comments on the same line as the code they're commenting on. Emacs has support for this, treating each of these a little differently. But especially on the Scheme side this is no longer as popular as it was, but the difference between ;; and ; is still common.

  • Most modern Schemes have adopted new kinds of comments: theres:

    • #|...|# for a block comment -- useful for long pieces of text that comment on the whole file.
    • #;<expr> is a comment that makes the implementation ignore the expression, which is useful for debugging.
  • As for the actual content of what to write, that's not different than any other language, except that with a more functional approach you usually have more choices on how to lay out your code. It also makes it more convenient to write smaller functions that are combined into larger pieces of functionality -- and this changes the documentation style too, since many such small functions will be "self documenting" (in that they're easy to read and very obvious in how they're working).

  • I hate to sound like a broken record, but I still think that you should spend some time with HtDP. One thing that it encourages in its design recipe is to write examples first, then the documentation, and then expand that to actual code. Furthermore, this recipe leaves you with code that has a very standard set of comments: the input/output types, a purpose statement, some documentation about how the function is implemented when necessary, and the examples can be considered as another kind of documentation (which would turn to commented code in "real" code). (There are other books that take a similar position wrt documentation.)

  • Finally, documenting macros is not different than documenting any other code. The only thing that can be very different i what's written in the comments: instead of describing what some function is doing, you tend to describe what code it expands too, so the comments are also more on the meta level. A common approach to macros is to to minimal work inside the macro -- just what's needed at that level (eg, wrap expressions in (lambda () ...)), and leave the actual implementation to a function. This helps in documenting too, since the two related pieces will have comments on how the macro expands and how it runs, independently.

Eli Barzilay
+1, Thanks - helpful as usual. In particular I appreciate the second and last points.
Cam
+5  A: 

The common style for Lisp comments is

  • Four semicolons for commentary on a whole subsection of a file.
  • Three semicolons for introducing a single procedure.
  • Two semicolons for a description of the expression/procedure definition on the following line.
  • One semicolon for an endline comment.

Procedure overview comments should probably follow the style of RnRS documens, so to just add comments to your procedure as-is, would look something like

;;; Procedure: display-n NUM ...
;; Output each argument to the screen in the order they are provided.
(define 
  display-n (lambda nums
              (letrec ((display-n-inner (lambda (nums)
                                          (display (car nums))
                                          (if (not (equal? (cdr nums) '()))
                                              (display-n-inner (cdr nums))))))
                (display-n-inner nums))))

N.B. I don't use three semicolons for the whole procedure description, since it screws up fill-paragraph in Emacs.


Now about the code, I would ditch the whole define-variable-as-a-lambda thing. Yes, I get that this is the "purest" way to define a function, and it makes for a nice consistency with defining procedures are the results of LETs and other procedures, but there's a reason for syntactic sugar, and it's to make things more readable. Same for the LETREC—just use an internal DEFINE, which is the same thing but more readable.

It's not a huge deal that DISPLAY-N-INNER's parameter is called NUMS, since the procedure's so short and DISPLAY-N just hands its NUMS straight to it anyways. "DISPLAY-N-INNER" is sort of a lame name, though. You would give it something with more semantic meaning, or give it a simple name like "ITER" or "LOOP".

Now about the logic of the procedure. First, (equal? (cdr nums) '()) is silly, and is better as (null? (cdr nums)). Actually, when you are operating over an entire list, it's best to make the base case a test of whether the list itself, and not its CDR, is empty. This way the procedure won't error if you pass it no arguments (unless you want it to do that, but I think it makes more sense for DISPLAY-N to do nothing if it gets nothing). Furthermore, you should test whether to stop the procedure, not whether to continue:

(define (display-n . nums)
  (define (iter nums)
    (if (null? nums)
        #t  ; It doesn't matter what it returns.
        (begin (display (car nums))
               (iter (cdr nums)))))
  (iter nums))

But for all that, I would say the the procedure itself is not the best way to accomplish the task it does, since it is too concerned with the details of traversing a list. Instead you would use the more abstract FOR-EACH method to do the work.

(define (display-n . nums)
  (for-each display nums))

This way, instead of a reader of the procedure getting mired in the details of CARs and CDRs, he can just understand that FOR-EACH will DISPLAY each element of NUMS.

Cirno de Bergerac
+1, all of that is good advice (especially the part about syntactic sugar). Also, tbh I actually didn't know about `null?` - so that's useful. Regarding for-each I realize that's the best way to actually tackle this particular problem, but then my code snippet would have been too trivial :)
Cam
As another intermediate form, why use the inner procedure? You can make the outer recursive without loss of encapsulation.
Svante
@Svante: It's because the outer `nums` is a list made up of all the arguments provided to the function. Calling the outer procedure recursively would only provide one parameter - the arguments in the form of a list. The problem there is that `display-n` actually expects multiple arguments - not one argument which is a list, so it wouldn't work. That being said, your suggestion would be possible using `apply` - except I'm not sure how efficient `apply` is, so that still might not be a good solution.
Cam
@incrediman: use `apply`. How could it not be efficient?
Svante
@Svante: Like I said, I'm unsure of how efficient apply is. If it takes O(1) time, then it's fine of course. In this situation it would definitely be possible to have it take O(1) time, if the interpreter were to realize that the function's parameter 'converts' the arguments back to a list anyways. However I worry that instead, it might run `apply` on the entire list, and then call the function with those arguments, and finally convert the arguments back to a list.
Cam
@incrediman: `apply` simply applies the function. It is one of the most basic operations. There is simply no basis for your fear.
Svante
@Svante: Could you please explain the output of this code though (http://pastebin.org/393648)? I'm having trouble figuring out why it takes so long for the `apply` version to run, given what you're saying. The output seems to confirm my 'fears' above, but I don't want to jump to conclusions.
Cam
I tried if again except I gave L a length of 40k instead of 20k. Now f-no-apply takes `cpu time: 0 real time: 9 gc time: 0` and f-apply takes `cpu time: 77548 real time: 100984 gc time: 17148`. There might be something in my own code that's causing it to take so long, but I don't think so. Does apply actually iterate through the list before providing it to the function (as I suggested above)? Assuming there aren't any mistakes in that code, it would be helpful if someone could explain how `apply` is implemented (particularly in racket), as that might explain the output of my code.
Cam
I looked at that, and it seems that at least Racket and SBCL have `apply` freshly cons the spreadable argument list. The Common Lisp standard does not require this consing (and functions should thus not rely on their rest parameter being freshly consed); I am not sure about the Scheme standard. Another concern is that the list might be circular. I guess it boils down to how lambda lists are matched to the supplied arguments. Anyway, you would normally use a higher order function like `map` or `reduce` whenever you would have such long argument lists.
Svante
By the way, your `f-apply` does not need the inner function anymore.
Svante