Reasoning


I'm going to show you three pieces of code. Here's the first one:

# Ruby
def total_amount
  subtotal - (discount.rate * subtotal) + shipping
end

My simple question is, what does that code do? How do you call it? What does it return, what errors might it raise, and why? What other code could it call?

You don't know. You can't know. You think you know but you can't be sure: is subtotal a simple field accessor? Does it have logic of its own? What is discount returning, or even doing? Can it fail? Does it return a number, an array, an object? It returns something that answers to the rate message... There are sharp-edged subtleties to this that you can't tell until runtime.

Twenty years ago, I was in love with code like this. Along with the sharp edges comes a lot of power: Ruby's metaobject protocol lets you swap in and out pieces of this at will, intercept calls, have code that modifies its own behavior at runtime, and all those things have uses! Testing libraries, reflection on database schemata... Near the start of my career I worked at a place where people hand-typed Java classes that simply serialized fields to a database. Took about a day to make one, entirely mindless, and there were typos all over them. No automated testing or code review. Ruby ActiveRecord's ability to just do this in a few lines of code was amazing. And if you wrote a bug with it, it's your own damn fault; should have commented better.


Here's the second piece of code:

/* C */
double total_amount(Order *order) {
  struct Discount discount = discount_for(order);
  return order->subtotal - (discount.rate() * order->subtotal) + shipping(order);
}

We can tell some types now. We're returning a double, which means that subtotal (which is a member of a struct, not a function itself) must be at most a double (could be an int or a float and it would be silently cast up). We know Discount is a struct that we create, it contains a function pointer to something called rate. But we're still limited in what we can reason about. There are some ownership questions: does discount hold a pointer to order? Can I keep discount around after I free the order? What if shipping mutates something in the order, does that affect the calculated rate? Which is called first? Does it matter? Do I need to do anything to clean up from creating a new Discount?

Sure. Probably not. This is probably exactly what it looks like it is. Except that you have some weird bug report and a stack trace led you here, and you don't trust anything right now; anything could have weird behavior hiding behind it.


What you want, what you need, is to be able to rule things out. Clear the building, mark off sections as "this can't possibly be it, so I can ignore this." You need to be able to look at a piece of code and know what it does.

Maybe you say, well, you can do that in C, no problem, just have good style and don't write code that has hidden traps. That's what I said about Ruby 20 years ago, and it turns out not to be good enough: you have to have that discipline across everyone in the project, across hundreds of files and tens of thousands of lines, across potentially decades of development, including that one really weird shipping bug you put in that cursed fix for late one Friday afternoon and had every intention of revisiting on Monday. Including the horrible code that came out of that intern one summer when he was supposed to implement selling gift cards, which have weird discount-code rules, which no one understands but it seems to work so everyone's afraid to touch it.

Maybe you say, well, my codebase is intentionally kept small enough that it's never a problem. In which case, sure! Good for you! I hope you have fun working on your small project solving small problems, this is legitimately not relevant to you, this is not a problem you have. Programming is relevant to all software, but software engineering really only comes into play on large systems. Software engineering is the discipline of managing complexity. It's about making things reasonable, in the sense of "able to be reasoned about."


Here is a third piece of code:

// Rust
fn total_amount(order: &order) -> f64 {
  order.subtotal as f64 - (order.discount.rate_for(&order) * order.subtotal) as f64 + shipping(order)
}

This piece of code implies some things:

  • First, order.subtotal, which is a normal field, is not an f64, but it's a primitive type that can be cast into it.
  • order.discount must be some non-primitive that has a rate_for method. We know that method isn't going to alter the order in any way because it can't; it takes an &Order, not a &mut Order.
  • It also must be returning a primitive of the same type as subtotal or we'd have to explicitly cast.
  • shipping takes an &Order, which again it cannot mutate, and returns an f64. It has no side effects or cleanup we need to worry about or else we would have to deal with them.

While writing this I find myself imagining the rest of the system this goes with: discount is almost certainly an enum Discount with a rate_for that contains a big match statement of all the discount formulae...

Can this function hide a bug? Maybe... but unlikely. Nothing it calls is fallible, or else it would return Result or Option. From here I might look into whatever order.discount is, or whatever shipping does, but this seems like it can have no weird behavior hiding in it.

It's reasonable.


Programming is not a science. It's a form of engineering, a craft, an art form sometimes... Debugging is a science for sure. And to do science, you need to perform experiments, and be able to reason about the results. You need rules that are consistent and observable. You need defined behavior.

Good languages give you that. Bad languages give you footguns. Really bad languages look like they give you that, and actually give you foot-howitzers.