6P [[documentation]] is an approach to writing [[Documentation#Header documentation|header documentation]] that addresses the two main requirements of all documentation: - explaining program logic; and - outlining the contract between program and user. 6P was originally devised by [Prof. Sam Rebelsky](https://rebelsky.cs.grinnell.edu/) and is the style of header documentation I was taught at university. It is not on any UK exam board specification, nor is it required in the [[H446 NEA Overview|H446 NEA]]. However, proper application of 6P results in thinking about your code and its design in ways that are credited in the NEA. Writing 6P documentation as part of your design ticks the same boxes as tedious-as-hell function diagrams and sets you up to articulate your program logic formally and precisely. In this guide, I outline the structure of 6P and discuss each of the Ps in detail. All original credit for the Ps goes to Sam Rebelsky and his colleagues at Grinnell. ## Philosophy 6P operates under the following assumptions: - header documentation is written before code; - the writing of documentation is an act of design; - it should not be necessary to read the code of a subprogram to use it; and - the contract established by documentation binds both the user and the program. ## The Ps ### Procedure Simply the name of the procedure/subprogram. It is called 'procedure' rather than subprogram as Sam originally devised 6P for use with Scheme, a language in which all subprograms are procedures. Procedure is included for the sake of making your documentation as explicit as possible and to guarantee the assumption that reading the code of a subprogram is not needed to use it. ### Parameters A list of all the subprogram's [[parameter|parameters]] including: - [[identifier|identifiers]]; and - [[data type|data types]]. Identifiers are included so the parameters can be referred to clearly in the rest of the header documentation. Data types are included so users know what data types are valid for each parameter. This is helpful in languages like Python which are dynamically typed. In addition to including data types in your 6P documentation, you could include [[Type hints|type hints]]. As with [[#Procedure]], the above means it is never necessary to read the code of a subprogram to use it. ### Purpose What's the point of the subprogram? Don't write reams—a sentence or two should be sufficient. If you can't articulate this clearly and concisely, it's a sign that you need to reconsider your design. Perhaps you don't understand the problem you are trying to solve, or perhaps you're trying to pack too much functionality into a single subprogram. Remember the [[Single-responsibility principle|single-responsibility principle]]. ### Product Gives an identifier and a data type for the result (i.e., return value). This is useful as it allows us to discuss the result in our documentation and it forms part of the contract between user and program—guaranteeing the return data type. If your subprogram doesn't have an explicit `return` value, the product should be `None`[^1]. ### Preconditions A list of things that must be true for the subprogram to work as intended—in other words, a list of requirements. Sometimes, there are none, in which case we write something like `[No additional]`. Preconditions typically include value restrictions of parameters. By stating these restrictions in our documentation, we form a contract with the user—so we are not obliged to verify that our preconditions are met. However, it is often safer to verify anyway. To show if a precondition is verified or not, you can say in the documentation, e.g., `a >= 0 (verified)` or `10 <= a <= 20 (not verified)`. This is not required but is helpful, especially when there is a mix of verified and not verified preconditions. Preconditions should be expressed as formally and precisely as possible. Prose is less precise than mathematical expressions or pseudocode so should generally be avoided. Consider which is clearer: ```Python """ Parameters: a: int ... Preconditions: a is a number between 9 and 19 (not verified) """ ``` or ```Python """ ... Parameters: a: int ... Preconditions: 9 <= a <= 19 (not verified) """ ``` Mathematical precision always wins. Preconditions are only useful when they are comprehensive. Consider them carefully as they are the rules by which your users will play. ### Postconditions The counterpart to preconditions, postconditions describe what will be true after the subprogram runs. Postconditions are your promise to the user about what your subprogram will do. Postconditions do not describe your subprogram's implementation—just what will be true after it runs. Writing postconditions like this treats your subprogram like a [[black box]]. Postconditions should be expressed precisely and formally, using mathematical expressions or pseudocode as required. Even if your subprogram only has [[side effect|side effects]], your postconditions should describe them clearly. Postconditions are usually best explained with [[#Examples|examples]]. ## Examples ### `max()` function We want to write a subprogram that compares two numbers and returns the largest, i.e., the max(imum). You would think that writing correct documentation for `max()` would be trivial. However, when we start to think it through, some complications arise. Let's reason about `max()` and how it should work before we commit to anything. We know the name of the subprogram: `max`. We want to compare two numbers, so these will need to be our parameters. Let's call them `a` and `b` and say they have to be `int`s as we are working with numbers. The purpose has already been stated: finding the maximum value of two numbers—in our case, `a` and `b`. Our product needs a name. `result` seems descriptive enough. Are there any preconditions here? I don't think so. What will be true after the subprogram ends? `return` will be the largest value of `a` and `b`—but we need to write this formally… let's put it all together: ```python """ Procedure: max Parameters: a, int b, int Purpose: Find the maximum value of a and b. Product: result, int Preconditions: [No additional] Postconditions: result >= a result >= b result is either a or b """ ``` Great! We have our header documentation. Now, let's write our code: ```python def max(a, b): """ Procedure: max Parameters: a, int b, int Purpose: Find the maximum value of a and b. Product: result, int Preconditions: [No additional] Postconditions: result >= a result >= b result is either a or b """ if a >= b: return a else: return b ``` Except there is a problem: our documentation isn't entirely true. With the subprogram we wrote, neither `a` nor `b` has to be `int`s. They could be `float`s, or strings, or any other type that can be compared with the `>` operator. Now we must answer a question: does our documentation need to be true? It's the documentation that forms the contract with our users, not our code. We could therefore argue that our documentation is fine. I would argue, however, that our contracts should be honest. So we redraft. Now, the types of `a` and `b` are flexible and we require a precondition: ```python def max(a, b): """ Procedure: max Parameters: a, a comparable type b, a comparable type Purpose: Find the maximum value of a and b. Product: result, a comparable type Preconditions: a and b can be compared using > Postconditions: result >= a result >= b result is either a or b """ ``` See how writing 6P documentation forces you to think carefully about your design? ### `greet()` procedure We want a subprogram that prints a personalised greeting to the console, e.g., `greet("James")` outputs `Hello, James!` Let's reason about `greet()` before writing any code, as we did with `max()`. We know the name: `greet`. We need a name to greet, so we'll have one parameter, `name`, which should be a `str`. The purpose: print a personalised greeting to the console. What's our product? Unlike `max()`, `greet()` doesn't calculate anything to return—it just prints, so our product is `None`[^1]. Are there any preconditions? We could require `name` to be non-empty, but let's keep this example trivial and say there are none. We have just made a design choice about our subprogram, prompted by reasoning about it through writing documentation. What about postconditions? This is where `greet()` differs from `max()` in an important way. `max()`'s postconditions described facts about its return value. `greet()` has no meaningful return value—but it does have a [[side effect]]: it prints something to the console. Postconditions describe everything that's true after the subprogram runs, not only facts about a returned value. So we write: ```python """ Procedure: greet Parameters: name, str Purpose: Print a personalised greeting to the console. Product: None Preconditions: [No additional] Postconditions: "Hello, {name}!" is printed to the console. """ ``` Now we can write our code. ```python def greet(name): """ Procedure: greet Parameters: name, str Purpose: Print a personalised greeting to the console. Product: None Preconditions: [No additional] Postconditions: f"Hello, {name}!" is printed to the console. """ print(f"Hello, {name}!") ``` The key lesson here is that a `None` product does not mean no postconditions. If we'd written `Product: None` and stopped there, we'd have told the user nothing about what calling `greet()` actually does. The contract of this procedure lives in its postconditions—the side effect—not in its product. Some subprograms make their promise through what they return; others, like `greet()`, make it through what they do to the world. ### `__init__()` constructor We want a `BankAccount` class where every account has an owner and a balance, set when the account is created. We need a [[constructor]]. We did the right thing and made a class diagram first: ```mermaid classDiagram class BankAccount { +str owner -int __balance +init(p_owner, p_initial_balance) +deposit(p_amount) } ``` Unlike `max()` and `greet()`, we're not designing `__init__()` from a blank slate—we've already settled the design of `BankAccount` in its class diagram above. Writing 6P documentation here isn't so much an act of design, but an act of translation: turning a design we've already committed to into a formal contract. We know the name: `__init__()`. Our class diagram tells us that `__init__()` takes two parameters. Since these are constructor parameters, they get the `p_` prefix: `p_owner` and `p_initial_balance`[^2]. The purpose is to be the `BankAccount` object constructor. What's our product? You'd imagine it would be a `BankAccount` object—you'd be wrong. It's `None`[^3]. Are there any preconditions? It would be reasonable for a bank account to start with a non-negative balance, so let's say `p_initial_balance >= 0`. We've just made a design choice that our class diagram didn't include—this is fine, as class diagrams don't usually have this level of detail. What about postconditions? Like `greet()`, `__init__()`'s contract isn't expressed through a return value—it's expressed through side effects. But where `greet()`'s side effect was printing to the console, `__init__()`'s side effect is changing the [[object state|state]] of the object being created: its attributes now hold specific values. So we write: ```python """ Procedure: __init__ Parameters: p_owner, str p_initial_balance, int Purpose: Constructor for BankAccount objects Product: None Preconditions: p_initial_balance >= 0 (not verified) Postconditions: self.owner == p_owner self.__balance == p_initial_balance """ ``` Now, we write our code: ```python class BankAccount: def __init__(self, p_owner, p_initial_balance): """ Procedure: __init__ Parameters: p_owner, str p_initial_balance, int Purpose: Constructor for BankAccount objects Product: None Preconditions: p_initial_balance >= 0 (not verified) Postconditions: self.owner == p_owner self.__balance == p_initial_balance """ self.owner = p_owner self.__balance = p_initial_balance ``` `__init__()`'s postconditions aren't about what gets returned, but about what becomes true of the object's state once construction finishes. ## Comments on syntax There are no hard rules about the 'syntax' of your 6P documentation. General rules of effective documentation apply: - refer to variables by their identifiers where possible; - use indentation to aid readability; and - be consistent. ## Further reading - https://rebelsky.cs.grinnell.edu/musings/six-ps-2019-09-05 (blog-style post by Sam about 6Ps) - https://rebelsky.cs.grinnell.edu/Courses/CSC151/2016S/readings/documentation-reading.html (the reading I did originally at university) - https://weinman.cs.grinnell.edu/courses/CSC213/2014F/labs/documentation.html (another take by Prof. Jerod Weinman) ## Notes [^1]: See [[Python quirks#All subprograms return `None` by default]]. [^2]: See [[Public/About#p_parameter]]. [^3]: See [[Python quirks#Constructors return `None`, not the object]].