Shared posts

19 Jun 10:33

Write code that's easy to delete, and easy to debug too.

Debuggable code is code that doesn’t outsmart you. Some code is a little to harder to debug than others: code with hidden behaviour, poor error handling, ambiguity, too little or too much structure, or code that’s in the middle of being changed. On a large enough project, you’ll eventually bump into code that you don’t understand.

On an old enough project, you’ll discover code you forgot about writing—and if it wasn’t for the commit logs, you’d swear it was someone else. As a project grows in size it becomes harder to remember what each piece of code does, harder still when the code doesn’t do what it is supposed to. When it comes to changing code you don’t understand, you’re forced to learn about it the hard way: Debugging.

Writing code that’s easy to debug begins with realising you won’t remember anything about the code later.

Rule 0: Good code has obvious faults.

Many used methodology salesmen have argued that the way to write understandable code is to write clean code. The problem is that “clean” is highly contextual in meaning. Clean code can be hardcoded into a system, and sometimes a dirty hack can written in a way that’s easy to turn off. Sometimes the code is clean because the filth has been pushed elsewhere. Good code isn’t necessarily clean code.

Code being clean or dirty is more about how much pride, or embarrassment the developer takes in the code, rather than how easy it has been to maintain or change. Instead of clean, we want boring code where change is obvious— I’ve found it easier to get people to contribute to a code base when the low hanging fruit has been left around for others to collect. The best code might be anything you can look at quickly learn things about it.

  • Code that doesn’t try to make an ugly problem look good, or a boring problem look interesting.
  • Code where the faults are obvious and the behaviour is clear, rather than code with no obvious faults and subtle behaviours.
  • Code that documents where it falls short of perfect, rather than aiming to be perfect.
  • Code with behaviour so obvious that any developer can imagine countless different ways to go about changing it.

Sometimes, code is just nasty as fuck, and any attempts to clean it up leaves you in a worse state. Writing clean code without understanding the consequences of your actions might as well be a summoning ritual for maintainable code.

It is not to say that clean code is bad, but sometimes the practice of clean coding is more akin to sweeping problems under the rug. Debuggable code isn’t necessarily clean, and code that’s littered with checks or error handling rarely makes for pleasant reading.

Rule 1: The computer is always on fire.

The computer is on fire, and the program crashed the last time it ran.

The first thing a program should do is ensure that it is starting out from a known, good, safe state before trying to get any work done. Sometimes there isn’t a clean copy of the state because the user deleted it, or upgraded their computer. The program crashed the last time it ran and, rather paradoxically, the program is being run for the first time too.

For example, when reading and writing program state to a file, a number of problems can happen:

  • The file is missing
  • The file is corrupt
  • The file is an older version, or a newer one
  • The last change to the file is unfinished
  • The filesystem was lying to you

These are not new problems and databases have been dealing with them since the dawn of time (1970-01-01). Using something like SQLite will handle many of these problems for you, but If the program crashed the last time it ran, the code might be run with the wrong data, or in the wrong way too.

With scheduled programs, for example, you can guarantee that the following accidents will occur:

  • It gets run twice in the same hour because of daylight savings time.
  • It gets run twice because an operator forgot it had already been run.
  • It will miss an hour, due to the machine running out of disk, or mysterious cloud networking issues.
  • It will take longer than an hour to run and may delay subsequent invocations of the program.
  • It will be run with the wrong time of day
  • It will inevitably be run close to a boundary, like midnight, end of month, end of year and fail due to arithmetic error.

Writing robust software begins with writing software that assumed it crashed the last time it ran, and crashing whenever it doesn’t know the right thing to do. The best thing about throwing an exception over leaving a comment like “This Shouldn’t Happen”, is that when it inevitably does happen, you get a head-start on debugging your code.

You don’t have to be able to recover from these problems either—it’s enough to let the program give up and not make things any worse. Small checks that raise an exception can save weeks of tracing through logs, and a simple lock file can save hours of restoring from backup.

Code that’s easy to debug is code that checks to see if things are correct before doing what was asked of it, code that makes it easy to go back to a known good state and trying again, and code that has layers of defence to force errors to surface as early as possible.

Rule 2: Your program is at war with itself.

Google’s biggest DoS attacks come from ourselves—because we have really big systems—although every now and then someone will show up and try to give us a run for our money, but really we’re more capable of hammering ourselves into the ground than anybody else is.

This is true for all systems.

Astrid Atkinson, Engineering for the Long Game

The software always crashed the last time it ran, and now it is always out of cpu, out of memory, and out of disk too. All of the workers are hammering an empty queue, everyone is retrying a failed request that’s long expired, and all of the servers have paused for garbage collection at the same time. Not only is the system broken, it is constantly trying to break itself.

Even checking if the system is actually running can be quite difficult.

It can be quite easy to implement something that checks if the server is running, but not if it is handling requests. Unless you check the uptime, it is possible that the program is crashing in-between every check. Health checks can trigger bugs too: I have managed to write health checks that crashed the system it was meant to protect. On two separate occasions, three months apart.

In software, writing code to handle errors will inevitably lead to discovering more errors to handle, many of them caused by the error handling itself. Similarly, performance optimisations can often be the cause of bottlenecks in the system—Making an app that’s pleasant to use in one tab can make an app that’s painful to use when you have twenty copies of it running.

Another example is where a worker in a pipeline is running too fast, and exhausting the available memory before the next part has a chance to catch up. If you’d rather a car metaphor: traffic jams. Speeding up is what creates them, and can be seen in the way the congestion moves back through the traffic. Optimisations can create systems that fail under high or heavy load, often in mysterious ways.

In other words: the faster you make it, the harder it will be pushed, and if you don’t allow your system to push back even a little, don’t be surprised if it snaps.

Back-pressure is one form of feedback within a system, and a program that is easy to debug is one where the user is involved in the feedback loop, having insight into all behaviours of a system, the accidental, the intentional, the desired, and the unwanted too. Debuggable code is easy to inspect, where you can watch and understand the changes happening within.

Rule 3: What you don’t disambiguate now, you debug later.

In other words: it should not be hard to look at the variables in your program and work out what is happening. Give or take some terrifying linear algebra subroutines, you should strive to represent your program’s state as obviously as possible. This means things like not changing your mind about what a variable does halfway through a program, if there is one obvious cardinal sin it is using a single variable for two different purposes.

It also means carefully avoiding the semi-predicate problem, never using a single value (count) to represent a pair of values (boolean, count). Avoiding things like returning a positive number for a result, and returning -1 when nothing matches. The reason is that it’s easy to end up in the situation where you want something like "0, but true" (and notably, Perl 5 has this exact feature), or you create code that’s hard to compose with other parts of your system (-1 might be a valid input for the next part of the program, rather than an error).

Along with using a single variable for two purposes, it can be just as bad to use a pair of variables for a single purpose—especially if they are booleans. I don’t mean keeping a pair of numbers to store a range is bad, but using a number of booleans to indicate what state your program is in is often a state machine in disguise.

When state doesn’t flow from top to bottom, give or take the occasional loop, it’s best to give the state a variable of it’s own and clean the logic up. If you have a set of booleans inside an object, replace it with a variable called state and use an enum (or a string if it’s persisted somewhere). The if statements end up looking like if state == name and stop looking like if bad_name && !alternate_option.

Even when you do make the state machine explicit, you can still mess up: sometimes code has two state machines hidden inside. I had great difficulty writing an HTTP proxy until I had made each state machine explicit, tracing connection state and parsing state separately. When you merge two state machines into one, it can be hard to add new states, or know exactly what state something is meant to be in.

This is far more about creating things you won’t have to debug, than making things easy to debug. By working out the list of valid states, it’s far easier to reject the invalid ones outright, rather than accidentally letting one or two through.

Rule 4: Accidental Behaviour is Expected Behaviour.

When you’re less than clear about what a data structure does, users fill in the gaps—any behaviour of your code, intended or accidental, will eventually be relied upon somewhere else. Many mainstream programming languages had hash tables you could iterate through, which sort-of preserved insertion order, most of the time.

Some languages chose to make the hash table behave as many users expected them to, iterating through the keys in the order they were added, but others chose to make the hash table return keys in a different order, each time it was iterated through. In the latter case, some users then complained that the behaviour wasn’t random enough.

Tragically, any source of randomness in your program will eventually be used for statistical simulation purposes, or worse, cryptography, and any source of ordering will be used for sorting instead.

In a database, some identifiers carry a little bit more information than others. When creating a table, a developer can choose between different types of primary key. The correct answer is a UUID, or something that’s indistinguishable from a UUID. The problem with the other choices is that they can expose ordering information as well as identity, i.e. not just if a == b but if a <= b, and by other choices mean auto-incrementing keys.

With an auto-incrementing key, the database assigns a number to each row in the table, adding 1 when a new row is inserted. This creates an ambiguity of sorts: people do not know which part of the data is canonical. In other words: Do you sort by key, or by timestamp? Like with the hash-tables before, people will decide the right answer for themselves. The other problem is that users can easily guess the other keys records nearby, too.

Ultimately any attempt to be smarter than a UUID will backfire: we already tried with postcodes, telephone numbers, and IP Addresses, and we failed miserably each time. UUIDs might not make your code more debuggable, but less accidental behaviour tends to mean less accidents.

Ordering is not the only piece of information people will extract from a key: If you create database keys that are constructed from the other fields, then people will throw away the data and reconstruct it from the key instead. Now you have two problems: when a program’s state is kept in more than one place, it is all too easy for the copies to start disagreeing with each other. It’s even harder to keep them in sync if you aren’t sure which one you need to change, or which one you have changed.

Whatever you permit your users to do, they’ll implement. Writing debuggable code is thinking ahead about the ways in which it can be misused, and how other people might interact with it in general.

Rule 5: Debugging is social, before it is technical.

When a software project is split over multiple components and systems, it can be considerably harder to find bugs. Once you understand how the problem occurs, you might have to co-ordinate changes across several parts in order to fix the behaviour. Fixing bugs in a larger project is less about finding the bugs, and more about convincing the other people that they’re real, or even that a fix is possible.

Bugs stick around in software because no-one is entirely sure who is responsible for things. In other words, it’s harder to debug code when nothing is written down, everything must be asked in Slack, and nothing gets answered until the one person who knows logs-on.

Planning, tools, process, and documentation are the ways we can fix this.

Planning is how we can remove the stress of being on call, structures in place to manage incidents. Plans are how we keep customers informed, switch out people when they’ve been on call too long, and how we track the problems and introduce changes to reduce future risk. Tools are the way in which we deskill work and make it accessible to others. Process is the way in which can we remove control from the individual and give it to the team.

The people will change, the interactions too, but the processes and tools will be carried on as the team mutates over time. It isn’t so much valuing one more than the other but building one to support changes in the other.Process can also be used to remove control from the team too, so it isn’t always good or bad, but there is always some process at work, even when it isn’t written down, and the act of documenting it is the first step to letting other people change it.

Documentation means more than text files: documentation is how you handover responsibilities, how you bring new people up to speed, and how you communicate what’s changed to the people impacted by those changes. Writing documentation requires more empathy than writing code, and more skill too: there aren’t easy compiler flags or type checkers, and it’s easy to write a lot of words without documenting anything.

Without documentation, how can you expect people to make informed decisions, or even consent to the consequences of using the software? Without documentation, tools, or processes you cannot share the burden of maintenance, or even replace the people currently lumbered with the task.

Making things easy to debug applies just as much to the processes around code as the code itself, making it clear whose toes you will have to stand on to fix the code.

Code that’s easy to debug is easy to explain.

A common occurrence when debugging is realising the problem when explaining it to someone else. The other person doesn’t even have to exist but you do have to force yourself to start from scratch, explain the situation, the problem, the steps to reproduce it, and often that framing is enough to give us insight into the answer.

If only. Sometimes when we ask for help, we don’t ask for the right help, and I’m as guilty of this as anyone—it’s such a common affliction that it has a name: “The X-Y Problem”: How do I get the last three letters of a filename? Oh? No, I meant the file extension.

We talk about problems in terms of the solutions we understand, and we talk about the solutions in terms of the consequences we’re aware of. Debugging is learning the hard way about unexpected consequences, and alternative solutions, and involves one of the hardest things a programer can ever do: admit that they got something wrong.

It wasn’t a compiler bug, after all.

30 Jan 19:33

Differing philosophies of Python and PHP

Veky

Find 5 differences. :-)

Actual screenshot:

The screenshot shows the menu page of an Internet discussion
forum.  Two forum groups are in the screenshot. The most recent
discussion topic in the “PHP Development” group is “Hiding errors”.  The most recent
topic in the “Python Programming” group is “Handling errors”

02 Apr 22:01

/dev/null Follies

Veky

Yes! The most important file you ever have backupped. :-D

A Unix system administrator of my acquaintance once got curious about what people were putting into /dev/null. I think he also may have had some notion that it would contain secrets or other interesting material that people wanted thrown away. Both of these ideas are stupid, but what he did next was even more stupid: he decided to replace /dev/null with a plain file so that he could examine its contents.

The root filesystem quickly filled up and the admin had to be called back from dinner to fix it. But he found that he couldn't fix it: to create a Unix device file you use the mknod command, and its arguments are the major and minor device numbers of the device to create. Our friend didn't remember the correct minor device number. The ls -l command will tell you the numbers of a device file but he had removed /dev/null so he couldn't use that.

Having no other system of the same type with an intact device file to check, he was forced to restore /dev/null from the tape backups.

21 Feb 14:15

You Don't Want to Think Like a Programmer

by James Hague
Veky

Absolutely. :-)

It's an oft-stated goal in introductory coding books and courses: to get you to think like a programmer. That's better than something overly specific and low-level like "to learn Java." It's also not meant to be taken literally. A clearer, more accurate phrasing would be "to get you to break down problems in an analytical way." But let that initial, quirky sequence of five words--"to think like a programmer"--serve as a warning and a reminder.

Because you really don't want to think like a programmer.

It starts slowly, as you first learn good coding practices from the bad. Never use global variables; wrap all data into objects. Write getter and setter methods to hide internal representations. Use const wherever possible. Only one class definition per file, please. Format your source code to encourage reading and understanding by others. Take time to line up your equal signs so things are in nice, neat columns.

Eventually this escalates to thinking in terms of design patterns and citing rules from Code Complete. All these clueless people want you add features that are difficult and at odds with your beautiful architecture; don't they realize that complexity is the enemy? You come to understand why every time a useful program is written in Perl or PHP it's an embarrassment to computer science. Lisp is the way, and it's worth using even if you don't have access to most of the libraries that make Python such a vital tool. Then one day you find yourself arguing static versus dynamic typing and passionately advocating test-driven development and all hope is lost.

It's not that any of these things are truly bad on their own, but together they occupy your mind. You should be obsessing about the problem domain you're working in--how to make a game without pedantic tutorials, what's the most intuitive set of artistic controls in a photography app--and not endless software engineering concerns.

Every so often I see someone attempting to learn a skill (e.g., web design, game development, songwriting), by finishing a project every day/week/month. I love these! They're exciting and inspirational and immediate. What a great way to learn! The first projects are all about getting something--anything--working. That's followed by re-engineering familiar designs. How to implement Snake, for example. Or Tetris.

If you've embarked on such a journey, the big step is to start exploring your own ideas. Don't copy what people who came before you were copying from other people. Experiment. Do crazy things. If you stick to the path of building what has already been made, then you're setting yourself up as implementor, as the engineer of other people's ideas, as the programmer. Take the opportunity to build a reputation as the creator of new experiences.

And, incidentally, you know how to write code.

(If you liked this, you might enjoy Learning to Ignore Superficially Ugly Code.)
05 Apr 04:34

Deterministic voting is just too random

by david
Veky

Random? Yeah, right. :-)
In fact, it's a too simplified model, but still nicely shows the problems with our thinking.

One of the issues that often gets cited as an objection to random ballot is that it’s too random. What happens if a lunatic fringe gets voted in to power? People might get a majority simply by random chance.

This is just a brief note on how unlikely that is.

Suppose there are \(N\) seats, and we have a party with a fraction \(q\) of the vote. Let \(W_i\) be a random variable which is 1 if the party wins seat \(i\) and 0 if it loses. Let \(W = \sum W_i\) be the number of votes won and \(q_i = P(W_i = 1)\).

\(W\) is a sum of independent random variables. I’m going to assume that \(N\) is large enough that we can apply the law of large numbers and approximate it as a normal random variable.

So from now on we’ll pretend \[W \sim \mathrm{Normal}(qN, \sigma^2)\]

Then the probability of getting a majority is
\[
\begin{align*}
P(W > \frac{1}{2}N) & = P(Z > \frac{(\frac{1}{2} - q)N}{\sigma})\\
&= 1 - \mathrm{erf}\left((\frac{1}{2} - q)\sqrt{\frac{N}{q(1-q)}} \right) \\
\end{align*}\]

erf is monotonic increasing, so this value is maximized by making the argument as small as possible. Everything in there other than \(\sigma\) is fixed, so this is done by maximizing \(\sigma\), or equivalently \(\sigma^2\)

When does this happen?

Well \(W\) is a sum of independent random variables, so its variance is the sum of their variances. So \[\sigma^2 = \sum q_i(1 - q_i)\]

We want to maximize this subject to the constraint that \(\sum q_i = qN\). We can turn this constrained optimisation problem into an unconstrained one by adding a lagrange multiplier t. So we’re optimising the function \[f(q_i, t) = \sum q_i(1 - q_i) + t (\sum q_i - qN)\]

Taking partial derivatives we have

\[\frac{\partial f}{\partial q_i} = 1 - 2 q_i + t\]

Setting this to 0 we get

\[ q_i = \frac{1 - t}{2} \]

The significant part of this being that it’s a constant that doesn’t depend on i. So applying the constraint we have \(q = q_i\), and \(\sigma^2 = N q (1 – q)\).

(I think it’s probably possible to reach this conclusion without the normality assumption, but I haven’t thought through the details).

Thus
\[
\begin{align*}
P(\mathrm{majority}) & = 1 - \mathrm{erf}\left(\frac{(\frac{1}{2} - q)N}{\sigma}\right)\\
& = 1 - \mathrm{erf}\left((\frac{1}{2} - q)\sqrt{\frac{N}{q(1-q)}}\right)\\
\end{align*}
\]

At this point we’ll just plug some numbers in. Here’s some python code:

import math
from scipy.special import erf
 
def probability_of_majority(n,p):
    sd = math.sqrt(n * p * (1 - p))
    deviations_to_majority = n * (0.5 - p) / sd
    return 1 - erf(deviations_to_majority)

At this point let’s just plug some numbers in.

Consider the UK house of commons with its 650 seats. Suppose we have 40% of the vote. What are the chances of us getting a majority?

Plugging them into our python code we get that the answer is 1.84e-13. i.e. if we ran a general election 5 times a year between now and the point when homo sapiens first started to appear, it would still be extremely surprising if any parties with 40% of the vote had ever achieved a majority.

At 45% of the vote we have a probability of 0.00029. If we ran an annual election every year since the birth of Christ, it’d still be more likely than not this would never have happened but it wouldn’t be very surprising.

At 48% of the vote we’re up to 0.15. At this point we expect this to happen every few general elections.

So the lunatic fringe? They’re not going anywhere. The lunatic majority? Well, that’s what you get in a Democracy. But unless someone actually has a majority they’re not getting in.

Compare this to the percentage of the popular vote with which parties typically reach a majority in the UK under a deterministic system.

From this I can only conclude that deterministic voting (in a constituency based system) is simply too random to be practical.

flattr this!