TDDC74 Programming: Abstraction and Modelling Programming Style (updated jan 2015) Rules, Conventions, & Guidelines 1 Introduction In this course we will emphasize aspect of good programming style. This means both a) aspects that are specific to Scheme, and b) good programming in general. We also have some course-rules intended to help programmers learn some good habits (or help more experienced programmers unlearn some poor ones). Below are some brief guidelines to help you write readable and well-organized Scheme code. The guidelines are divided into four sections: PRAM Rules, Coding Restrictions, Scheme Conventions, and Style Guidelines. 2 THE IDEAL It shouldn’t take a lot of effort (for a reasonably knowledgeable) reader to ”decode” your code. The interpreter can handle valid code, however ugly or unwieldy. You don’t write programs only for the interpreter. You write it for other programmers. 3 PRAM RULES The following rules and restrictions apply for PRAM Lab Assignments and Quizzes: • No procedures or variables named “X”1 . Many bugs “magically vanish” when coders go through and replace too-short abbreviations with meaningful names. Not only that, but single-letter names make code extremely difficult for other people to read. WARNING! Students should expect that code may be returned, or quiz credits deducted, for this even if their code is otherwise correct. • Don’t overcomplicate things. We might remove points if it is clear that a student is trying to immediately ”optimize” the code rather than first implementing a correct algorithm. • Don’t write any Scheme code that relies on case distinctions. You can technically write code where eg the symbols myVar and MyVar refer to different values. But you shouldn’t. • Follow the ideal set out above. 4 CODING RESTRICTIONS DrRacket supports several non-Racket languages, and there are several flavours of Racket (eg Typed Racket, Lazy Racket). In general, students should use the standard Racket language in this course. Write #lang racket. Unless otherwise noted in writing on a lab assignment or a quiz, students may only use: 1 Unless it can be very well justified. 1 • Racket primitives allowed for that lab or quiz, plus all Scheme primitives introduced previous to the current lab or quiz. Labs will contain lists of these allowed primitives. • Any procedures the students build themselves from those primitives 5 SCHEME CONVENTIONS In addition to specific PRAM rules, there are some Scheme conventions that students should follow: Predicates (ie, “recognizers”) Programmers expect any Scheme procedure-name that ends in a question mark to be a recognizer – that is, to return true (#t) or false (#f). Thus, number? is a recognizer. Recognizers are also known more commonly as predicates. Do not use question marks to end the names of procedures that return other kinds of values! Do not create predicates that do other things when run (change data etc). Thus, even though this is a perfectly useful procedure, it is poorly named: (define (how-many-elements? lst) <code>) (how-many-elements? ’(a b c)) -> 3 A better name would be something like length or get-length. Note that >, <, and = are also recognizers, but Scheme follows the historical convention and does not add the question mark to them.2 Highlight Global Variables Global variables are accessible to all procedures, which means one procedure can modify them in ways that cause problems for other procedures. In order to remind ourselves to minimize the use of global variables, and to be careful when we do use them, it is a common convention to include an asterisk (“splat”) at each end of the name. (define *pi* (/ 22 7)) Commands Racket commands – that is, procedures that change the values bound to names – usually end with an exclamation-mark (ie, “bang”): set!, add-item-to-database!, etc. (Note that define is a command, although its name does not end with a bang.)3 Note that the return value for a command is unspecified – and, in general, most the applications of most Scheme commands do not display returned values. If you write a command of your own, don’t write code that relies on its return value. 6 STYLE GUIDELINES Here are some notes about good programming style. Unless otherwise indicated, we will not be obsessive about the guidelines. But we will return labs for completion (or remove points on a quiz) if we feel that code violates good style too much. Short Procedures Short procedures are easier to read, write, test, debug, and understand – especially when each procedure provides a single, well-defined operation. If procedures start to do several things, break them up into separate procedures. A good rule of thumb is that a procedure shouldn’t be longer than a screen full of code. 2 It would probably be more obvious if they had the following names, greater-than?, less-than?, equal-to?. We will not introduce commands until SICP 3 – and unless otherwise noted, students may not use them until then. 3 2 Good test: will someone need to spend time “deciphering” what a procedure is meant to do? No one – not other programmers and not you, yourself, six months after you write the code – should need to work hard to understand what a procedure is supposed to do.) Page-width is 80 characters A related rule is that code should not be wider than 80 characters. As a rule of thumb, hard-copy printouts of code should be well-formatted and easy to read on the printed page. On the screen, expect that the person reading your code may do it on a small screen. Indentation & Whitespace Within the Lisp/Scheme community there is a particular style associated with the use of parentheses, indentation and white-space; namely, to reflect the structure of the code. In particular, please do not use the “hanging parenthesis style” (typical of Java) for Scheme code in this course. GOOD: (define (foo x y) (let ((z (+ x y 10))) (* z z))) BAD: (define (foo x y) (let ((z (+ x y 10)))(* z z))) (define (foo x y) (let ( (z (+ x y 10) (* z z) ) ) ) ) Abstract common patterns/code into new procedures It is often the case that certain patterns appear in the process of developing different parts of an application. It improves the overall readability, reliability, and, yes, elegance of a program to abstract these patterns, thereby reducing the amount of code and increasing the visibility of the program’s structure. No Variables/Procedures Named X Do we repeat ourselves? :-) Use descriptive variable and function names. In most cases, it should be clear from a procedure’s name what the procedure is meant to do – and each procedure should only do one well-defined thing. (define *pi* (/ 22 7)) (define *pi-approx* (/ 22 7)) Of course, a little common sense is important: it is not necessary (or usually a good idea) to create extremely long names – so don’t create absurdly-long “self-documenting” names in order to avoid commenting the code (gasp!). (define *it-is-overkill-to-create-such-a-long-variable-name-for-pi* (/ 22 7)) (define *pi-approximation-correct-to-the-tenth-decimal-maybe* (/ 22 7)) Note that Scheme will allow you to use semi-colons for comments within programs. When commenting code, you can assume that the person reading your code knows Scheme – but does not know (or remember) what this exact code does. If the code is complex, it is much more helpful to have information about what the code is supposed to accomplish, rather than a detailed description of how the code works. Thus, comments should not be “text versions” of the code/algorithm itself. Example of clear, concise, helpful comment: (define (gcd num1 num2) ;; This returns the greatest common divisor of 2 numbers (...)) 3 Example of long, unhelpful commenting: (define (gcd num1 num2) ;; this program first determines whether num1 divided by num2 is ;; zero; if so, then it returns num1 ;; if not, then it ... ;; blah blah blah (...)) Use Helper Procedures Programmers should use helper-procedures to abstract away implementation details – and to create appropriate abstract interfaces to different levels of programs. For example, if a program is going to be working with records of the form (GIVEN-NAME FAMILY-NAME DATE-OF-BIRTH), it is very difficult to read and debug code if it all consists of long compositions of car and cdr. Instead, create and make use of helper-procedures: (define (get-given recrd) (car recrd)) (define (get-family recrd) (cadr recrd)) (define (get-dob recrd) (caddr recrd)) Creating abstractions such as this takes no time at all – and dramatically reduces the number of bugs while also increasing the speed of implementation. Create Good Test-Cases A very important part of programming involves the development and use of suitable test-cases. You will get some automatic tests for free for some labs. During development, test your code on more/other values than the illustrative examples included in the lab document. Try to think of different kinds of corner cases that might be tricky, in order to convince yourself that your implementation is correct. Local Variables Are Also Your Friends Use local variables in preference to global variables whenever possible. Don’t use global variables as a substitute for parameter passing. Your programs should be as modular as possible. In keeping with old style, use let for local names, except in the cases of local procedure or, possibly, port definitions. Don’t overuse local variables, but note that they are sometimes useful for the sake of readability, or to save computations (instead of running a calculation twice, save the result temporarily). Use Appropriate Paradigmand don’t over-use assignment Use functional and imperative techniques as appropriate. In this course when we want procedures to “remember” certain values between different procedure-calls, we will often use assignment. Otherwise, the general philosophy of the course is to keep referential transparency by using basic recursion. Choose wisely. An example of inappropriate: using assignment and looping to update the value of a variable-binding when the same result can be achieved with recursion, unless asked to. Similarly, it is often a sign that there are problems if all of a student’s code starts to look “blocklike:” (define (some-assignment-based-procedure foo) (set! foo some-val) (set! foo (+ foo some-val)) (set! bar something-else) (set! bar (+ foo bar)) (do-something-with foo bar)) It is ok to use let if it makes the code more readable.4 4 The example block above can, however, easily be described as (define (some-assignment-based-procedure foo) (do-something-with (* 2 some-val) (+ some-val something-else))) 4