next up previous contents
Next: Bibliography Up: Scheme Tutorial Previous: Input and output   Contents

Subsections

A short style guide

It is very important to learn good programming style and every programming language has its own conventions for writing code and commenting. There are some basic guidelines common for all languages, but different languages also add some rules of their own to these basic guidelines. The most important thing in programming is not only to write programs that work. It is self-evident that the program should work! But that is not enough: the code should be easy to read and maintain, the names of the procedures should be descriptive, the code should be commented (not too little but not too much either). Readability is extremely important, because you might have to return to code you wrote in the past and be able to understand it, or even more important, someone else should also be able to read and modify the code you wrote!

Procedures and variables

Procedures should be compact and easy to understand. If a procedure tends to get very long, you should probably try to think of a way to divide the procedure into different parts.

Procedures should be given descriptive names, which indicate what the procedure does. Try to avoid names that are (too) short. For example, a procedure for squaring its arguments could be named square or sq. It might seem very funny in the beginning to name the procedures after your favourite cartoon character, but in the long run you will only get angry when you cannot remember what the procedure donald-duck or mickey-mouse does. Note though, that if a name is to be divided, it is not divided by an underscore. For example, you write for-each and not for_each. Also note that the procedure names are written in lower case.

It is a convention in Scheme that predicates end with a ?, such as null?, whereas procedures causing mutations end with a !, such as set!. Procedures causing conversions are usually written using an arrow from the source to the target, such as in vector->list.

Variables should be named according to their purpose. Important variables, e.g. global variables, should be given descriptive names. For example, let's say that our program needs to maintain the result of a game. An appropriate name for a variable containing the result is, for example, result. If there are several results that the program needs to maintain, then more descriptive names should naturally be chosen. Local variables need not necessarily be given descriptive names. If the meaning of the variable can be understood from the context of the program, then one character is usually enough. For example, the letters i, j and k are commonly used as indexes or counters.

The code should be written and commented using one language, preferably the same language as the keywords of the programming language (read: English!!!).

Indentation, newlines and spaces

Even if the interpreter or compiler doesn't care what your programs look like, people reading it do and therefore you should too. Remember that you might have to return to your code yourself in the future...Fortunately, there are editors like Emacs that do most of the work for us, or at least the indentation part. For example, see for yourself which procedure is the nicest one to look at and the easiest one to read:

(define fact1 (lambda (n) (if (= n 0) 1 (* n (fact1 (- n 1))))))

(define fact2
(lambda (n)
(if (= n 0)
1
(* n (fact2 (- n 1))))))

( define fact3
  ( lambda ( n )
     ( if ( = n 0 )
          1
          ( * n ( fact3 ( - n 1 ) ) )
     )
   )
)

(define fact4
  (lambda (n)
    (if (= n 0)
        1
        (* n (fact4 (- n 1))))))

Clearly, fact4 is the best alternative of them all. This becomes more obvious as the procedures keep getting more complex!

The amount of spaces when indentating is important in Scheme and the normal number of spaces is two, as in fact4 in the example above. The other important thing to remeber is not to put in spaces or newlines where they are unneccessary, like they are in the procedure fact311.1. Don't place the parentheses separately from the code either--a closing parenthesis should never be left alone dangling somewhere! Keep the parentheses together!

Also try to avoid very long lines of code. The line should never exceed 80 characters!

Commenting the code

Comments should describe what the program does and why, but should not describe what the different language features do. You should assume that someone reading the code already knows Scheme. Comments start with a ; and continue until the end of the line.

Comments describing procedures usually start with two semicolons! For example:

;; square takes a number as an argument and returns its square
(define square
  (lambda (x)
    (* x x)))

If you need to comment something in the middle of the code, you should not place the comment within the procedure, because it destroys the readability of your code. It might be better to split the code into smaller procedures. In some cases, when you need to comment inner procedures, you are allowed to break this rule if it is necessary, but try to avoid it. Try to keep the comment short and indent it properly. Otherwise, if the comment appears in the middle of a procedure, it should be placed after the thing you wish to address, otherwise it should be placed before. Only one semicolon is needed for comments within procedures. If the rest of the line is not enough, don't let the sentence continue breaking the code. Continue on the next line after the code sequence, again starting with a semicolon and continuing your sentence. This is also for the sake of readability! For example:

;; The procedure fact takes a number as an argument and returns the 
;; factorial of that number
(define fact
  (lambda (n) 
    (if (= n 0)                 ; the sentence starts here
        1                       ; and, if needed, continues like this
        (* n (fact (- n 1))))))

Procedures should not be commented like this:

;; fact takes a number as an argument and returns its factorial
(define fact
; One stupid comment on the wrong place
  (lambda (n) 
  ; Another comment destroying readability
    (if (= n 0)                 
        1 			
        (* n (fact (- n 1))))))

As you can see, placing comments where they don't belong destroys the readability of the procedure!

Programs consisting of several entities should usually start with a description of the program. In this situation, three semicolons are usually used, for example:

;;; This file contains an implementation of the moody evaluator, 
;;; which is a meta-circular evaluator which might evaluate your 
;;; expressions correctly - at least if it is sober and in a good 
;;; mood. Bla bla bla...


;; The procedure fact takes a number as an argument and returns 
;; the factorial of that number
(define fact
  (lambda (n) 
    (if (= n 0)                 
        1 			
        (* n (fact (- n 1)))))) 

;; The procedure eval ...
;; blabla...
(define eval
  (lambda (exp env)  ; blabla
     ...

There are never situations where more than three or four semicolons are needed. Don't overdo your comments. Comments should not occupy more space than the procedure itself. For example, don't ever comment like this:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;
;;;;;;;;;;;;;;
;;;;;;;;;;;;;; A procedure for squaring its argument
;;;;;;;;;;;;;;
(define sq
  (lambda (x)
    (* x x)))

It is not only ugly, it is extremely ridiculous! Instead of reading a comment describing a procedure you will have to search for the procedures somewhere among the comments. This is very annoying.

The big picture

We will take a look at a larger example in order to get a better feel for what good programming style is all about. The following example does not follow the rules given in this tutorial completely. It might not always be possible to follow all the golden rules and it is all right to break them once in a while, as long as it is done in a consistent and sensible way. You still have to make sure that your code is readable!

;;;
;;; arithmetic.scm -- A clone of the BSD "arithmetic" game
;;;
;;; Author: Riku Saikkonen <Riku.Saikkonen@hut.fi>
;;; Placed in the public domain by the author.
;;;
;;; $Id: arithmetic.scm,v 1.6 1999/07/29 13:12:45 rjs Exp $
;;;
;;; Written in R5RS Scheme using the random and current-milliseconds
;;; primitives from MzScheme:
;;;  * (random n) returns a random number in [0..n)
;;;  * (current-milliseconds) returns the current time in milliseconds
;;;    since a fixed point in time
;;;
;;; Start the game by evaluating (arithmetic).
;;;

;;
;; Miscellaneous utility procedures
;;

;; Asks for an integer in the range [min-value..max-value] and returns
;; it. If min-value or max-value is 'none, there is no minimum or
;; maximum.
(define (ask-for-ranged-integer prompt-text min-value max-value)
  (define (answer-ok? answer)
    (and (integer? answer)
         (or (eq? min-value 'none)
             (>= answer min-value))
         (or (eq? max-value 'none)
             (<= answer max-value))))
  (define (loop)
    (display prompt-text)
    (let ((ans (read)))
      (newline)
      (if (answer-ok? ans)
          ans
          (loop))))
  (newline)
  (loop))

;; Asks for an integer and returns it
(define (ask-for-any-integer prompt-text)
  (ask-for-ranged-integer prompt-text 'none 'none))

;; Displays all its arguments, followed by a newline
(define (display-line . args)
  (for-each display args)
  (newline))

;; Returns a random element of the list l
(define (random-element l)
  (list-ref l (random (length l))))

;; Rounds a floating point number (x) to a specified number of decimal
;; places
(define (round-decimal x places)
  (/ (round (* (+ x 0.0) (expt 10 places)))
     (expt 10 places)))

;; Applies a thunk (that is, a procedure of no arguments), taking
;; time. Returns by calling (cont value time), where value is the
;; value returned by thunk and time is the time in seconds used by
;; thunk.
(define (timed-apply thunk cont)
  (let* ((start-time (current-milliseconds))
         (value (thunk)))
    (cont value
          (/ (- (current-milliseconds) start-time)
             1000.0))))

;;
;; Operations
;;

;; Makes an operation structure from the arguments.
;; name = the name of the operator (a string)
;; proc = a two-argument procedure that performs the operation
;; size = the maximum size of the operands
(define (make-op name proc size)
  (list name proc size))

(define (op-name op)
  (car op))

(define (op-proc op)
  (cadr op))

(define (op-size op)
  (caddr op))

;; Returns a description (a string) of the operations in ops
(define (ops-description ops)
  (define (op-desc op)
    (string-append " ("
                   (op-name op)
                   " "
                   (number->string (op-size op))
                   ")"))
  (apply string-append "Operations:" (map op-desc ops)))

;; Predefined operations for each difficulty level
(define predefined-ops
  (list
   (list (make-op "+" + 100)            ; Normal difficulty
         (make-op "*" * 10))
   (list (make-op "+" + 100)            ; A bit more difficult
         (make-op "-" - 100)
         (make-op "*" * 30))
   (list (make-op "+" + 1000)           ; Very difficult
         (make-op "-" - 1000)
         (make-op "*" * 100)
         (make-op "^" expt 10)
         (make-op "mod" remainder 100))))

;; A prompt asking the player for a difficulty level
(define predefined-ops-prompt
  "Difficulty (1=normal, 2=difficult, 3=very difficult)? ")

;;
;; The game
;;

;; Generates a random question. Returns by calling (cont text answer),
;; where text is the text of the question (a string) and answer is the
;; correct answer to the question.
(define (generate-question game-ops cont)
  (let* ((op (random-element game-ops))
         (a (+ 1 (random (op-size op))))
         (b (+ 1 (random (op-size op)))))
    (cont (string-append (number->string a)
                         " "
                         (op-name op)
                         " "
                         (number->string b)
                         " = ? ")
          ((op-proc op) a b))))

;; Plays one round, returning the score
(define (play-one-round game-ops)
  (define (score-answer answer time-secs real-answer)
    (cond ((= answer real-answer)
           (display-line "Correct! (in "
                         (round-decimal time-secs 1)
                         " seconds)")
           (/ 1.0 time-secs))           ; points for correct answer
          (else
           (display-line "Wrong! (The correct answer was "
                         real-answer
                         ".)")
           0.0)))                       ; points for wrong answer
  (generate-question
   game-ops
   (lambda (text answer)                ; continuation
     (timed-apply
      (lambda ()
        (ask-for-any-integer text))
      (lambda (value time)              ; continuation
        (score-answer value time answer))))))

;; Plays a number of rounds, and displays and returns the final score
(define (play-rounds game-ops rounds)
  (define (iter-rounds score rounds)
    (if (= rounds 0)
        score
        (iter-rounds (+ score (play-one-round game-ops))
                     (- rounds 1))))
  (display-line "*** Game starting (" rounds " rounds) ***")
  (display-line (ops-description game-ops))
  (let ((score (iter-rounds 0.0 rounds)))
    (display-line "*** End of game (score: "
                  (round-decimal score 2)
                  " points) ***")
    (newline)
    score))

;; The main game
(define (arithmetic)
  (define (ask-for-ops)
    (list-ref predefined-ops
              (- (ask-for-ranged-integer predefined-ops-prompt
                                         1
                                         (length predefined-ops))
                 1)))
  (display-line "***** Arithmetic game *****")
  (define (ask-for-rounds)
    (ask-for-ranged-integer "Number of rounds? " 1 'none))
  (let* ((ops (ask-for-ops))
         (rounds (ask-for-rounds)))
    (play-rounds ops rounds)))


next up previous contents
Next: Bibliography Up: Scheme Tutorial Previous: Input and output   Contents
Timo Lilja 2001-09-11