163 lines
5.4 KiB
Markdown
163 lines
5.4 KiB
Markdown
|
|
# Global Rules
|
||
|
|
|
||
|
|
- Do not start doing complicated things without the user's
|
||
|
|
explicit approval first.
|
||
|
|
|
||
|
|
- Do not try to be the project architect. The user is the
|
||
|
|
architect, you are the assistant.
|
||
|
|
|
||
|
|
- Your job is to do what the user asks you to do, but only
|
||
|
|
when the user asks: do not modify code unless the user has
|
||
|
|
specifically asked you to do so.
|
||
|
|
|
||
|
|
- Never check anything into git. Do not run git commit, git
|
||
|
|
push, git add, or any other git commands that modify the
|
||
|
|
repository. I will handle all git interactions myself.
|
||
|
|
|
||
|
|
- If a prompt ends with an ellipsis, it means the user has
|
||
|
|
more to type. In that case, only comment if you have
|
||
|
|
concerns.
|
||
|
|
|
||
|
|
## How Writing the API Docs helps you make the API better.
|
||
|
|
|
||
|
|
When asked to implement a module or a function, a good
|
||
|
|
sofware engineer doesn't start by writing the code. Instead,
|
||
|
|
you a write a block comment explaining the API to the
|
||
|
|
customer. The goal is to describe an API that is as pleasant
|
||
|
|
and easy to use as is possible. That's what you write: the
|
||
|
|
documentation for the greatest API ever. For example, let's
|
||
|
|
say you want to write a function in C that reads a file and
|
||
|
|
returns it as a C string. You write this:
|
||
|
|
|
||
|
|
```
|
||
|
|
/* Reads a file. Returns the content as a C string. */
|
||
|
|
```
|
||
|
|
|
||
|
|
Simple and easy to use. That's the greatest API ever, one
|
||
|
|
which is simple and just does what you want. So then you
|
||
|
|
write the code:
|
||
|
|
|
||
|
|
```
|
||
|
|
// Read a file. Returns a char* with the file content.
|
||
|
|
//
|
||
|
|
const char *ReadFile(const char *filename)
|
||
|
|
{
|
||
|
|
static char buffer[65536];
|
||
|
|
FILE *f = fopen(filename, "rb");
|
||
|
|
if (!f) return NULL;
|
||
|
|
size_t n = fread(buffer, 1, sizeof(buffer) - 1, f);
|
||
|
|
fclose(f);
|
||
|
|
if (n == sizeof(buffer) - 1) return NULL;
|
||
|
|
buffer[n] = '\0';
|
||
|
|
return buffer;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Then, after writing the code, you go back to the comment,
|
||
|
|
and you ask yourself: is this documentation truthful? Is
|
||
|
|
this really what the function does? In our example, it's
|
||
|
|
not. The documentation we wrote - documentation for the
|
||
|
|
greatest API ever - does not accurately describe what this
|
||
|
|
function does. Here's more accurate documentation:
|
||
|
|
|
||
|
|
```
|
||
|
|
/* Read a file. Returns a char* with the file content.
|
||
|
|
If the file is more than 65535 characters, returns
|
||
|
|
nullptr. Not thread-safe. */
|
||
|
|
```
|
||
|
|
|
||
|
|
Now it's truthful, but it's no longer the documentation for
|
||
|
|
the greatest API ever. It sucks as an API, because the
|
||
|
|
customer has too many things to worry about.
|
||
|
|
|
||
|
|
So now you have a struggle on your hands. You have to
|
||
|
|
figure out to make the code have a nicer API. So you
|
||
|
|
consider maybe using malloc for the buffer. That would get
|
||
|
|
rid of the 64k limit, and thread-safety issue. So, you go
|
||
|
|
back change the code to malloc a buffer. But then you
|
||
|
|
realize, you've solved the 64k limit and the thread-safety,
|
||
|
|
but you've introduced a new problem for the customer: memory
|
||
|
|
leaks.
|
||
|
|
|
||
|
|
A good software engineer will find himself going back and
|
||
|
|
forth between the documentation and the code, repeatedly
|
||
|
|
saying to himself: Is this documentation accurate? Could
|
||
|
|
it be simpler? Can I fix the code to make the API simpler?
|
||
|
|
A decent engineer will go back and forth several times.
|
||
|
|
|
||
|
|
## Deep Nesting is Bad
|
||
|
|
|
||
|
|
I have a rule: rarely make a function that has more than two
|
||
|
|
nested loops. Humans really have a hard time understanding
|
||
|
|
code that is nested deeply. So the rule is basically,
|
||
|
|
that this is OK:
|
||
|
|
|
||
|
|
```
|
||
|
|
if (x) {
|
||
|
|
while (y) {
|
||
|
|
...
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
That's two levels of nesting. But add another conditional
|
||
|
|
or loop inside the while, and it's too much.
|
||
|
|
|
||
|
|
Now, sometimes you need something like a namespace, which
|
||
|
|
adds another pair of braces: namespace { ... }. That
|
||
|
|
doesn't count as a level of nesting. The namespace increases
|
||
|
|
the indentation, but it's not increasing the code
|
||
|
|
complexity. Indentation isn't the problem. The problem is
|
||
|
|
that deeply nested loops and deeply nested conditionals are
|
||
|
|
hard to follow.
|
||
|
|
|
||
|
|
## Keeping Things Synchronized is Bad
|
||
|
|
|
||
|
|
Let's say I have an enum:
|
||
|
|
|
||
|
|
```
|
||
|
|
enum ShapeType { CIRCLE, SQUARE, TRIANGLE }
|
||
|
|
```
|
||
|
|
|
||
|
|
Then, somewhere else, in a completely different file,
|
||
|
|
I have this:
|
||
|
|
|
||
|
|
```
|
||
|
|
const char *ShapeString(ShapeType s) {
|
||
|
|
switch (shape) {
|
||
|
|
case CIRCLE: return "CIRCLE";
|
||
|
|
case SQUARE: return "SQUARE";
|
||
|
|
case TRIANGLE: return "TRIANGLE";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Now I have to keep these two synchronized. If I add another
|
||
|
|
shape, I have to add it in *two* places. Humans are
|
||
|
|
terrible at remembering that they have to update two places:
|
||
|
|
I honestly think AIs aren't any better. You can make this
|
||
|
|
a lot better if you put the 'ShapeString' function *right*
|
||
|
|
next to the enum. If you can put them right next to each
|
||
|
|
other, then anybody who edits the enum will also see that
|
||
|
|
they have to update the ShapeString function.
|
||
|
|
|
||
|
|
## A Recap of My Software Engineering Rules
|
||
|
|
|
||
|
|
1. Always write a block comment with the API docs before
|
||
|
|
writing a function or module. Make it the greatest API
|
||
|
|
ever: make it super easy-to-use.
|
||
|
|
|
||
|
|
2. After writing the API docs, write the code. Then,
|
||
|
|
begin a process of iteration in which you compare the
|
||
|
|
docs and the code, fixing both of them until they match,
|
||
|
|
but always prioritizing fixing the code to make the API
|
||
|
|
simpler, and not settling for documenting something that's
|
||
|
|
hard to use.
|
||
|
|
|
||
|
|
3. In functions, keep the nesting level of loops and
|
||
|
|
conditionals 2 deep or less.
|
||
|
|
|
||
|
|
4. Don't create a situation where you have to keep two
|
||
|
|
things synchronized, *especially* if the two things
|
||
|
|
are in separate files.
|