Hacker News new | past | comments | ask | show | jobs | submit login
Principle of Least Astonishment (c2.com)
98 points by geerlingguy 10 months ago | hide | past | favorite | 46 comments

Yukihiro Matsumoto, creator of Ruby:

> Everyone has an individual background. Someone may come from Python, someone else may come from Perl, and they may be surprised by different aspects of the language. Then they come up to me and say, 'I was surprised by this feature of the language, so Ruby violates the principle of least surprise.'

> Wait. Wait. The principle of least surprise is not for you only. The principle of least surprise means principle of least my surprise. And it means the principle of least surprise after you learn Ruby very well. For example, I was a C++ programmer before I started designing Ruby. I programmed in C++ exclusively for two or three years. And after two years of C++ programming, it still surprises me.

For what it's worth, I don't think ruby achieves this goal either. I've been programming full time in Ruby for nearly three years and things like this [1] still surprise me. Ruby feels less surprising than C++, sure, but that's a pretty low bar to clear.

[1] https://makandracards.com/makandra/46939-ruby-a-small-summar...

Principles of least surprise (or astonishment), is one of cornerstones of Ruby. However, the language itself is dynamic, so take the end result with a pinch of salt: The language's OO syntax is beautifully simple, incredibly easy to structure into classes/mixins and have tons of useful libraries, to write code with. Not always so much for reading code, refactoring or discerning what REALLY goes on in a Rails application..

I think there is a great middle line for a language that is easy to work with, and provides enough safety to be maintainable for a long time. Environments like C++ and enterprise Java miss this middle line by being too cumbersome. However, Ruby equally misses this middle line, just in the other direction - it's too simple and dynamic leading to lack of maintainability in the long run. The best solution will be somewhere in between.

In my opinion, Kotlin, C#, and TypeScript all come pretty close.

My pet-peeve in ruby:

  '//'.split('/') # => [] instead of ['', '', '']
  '/'.split('/') # => [] instead of ['', '']
  ''.split('/') # => [] instead of ['']

  '//'.split('/', -1) # => ['', '', '']
  '/'.split('/', -1) # => ['', '']
  ''.split('/', -1) # => [] instead of ['']
In comparison:

  "".split("/") # => [""] in javascript
  "".split("/") # => [""] in python

But if "instead of" means "I expected," doesn't that also mean that Javascript and Python don't do what you expected?

JS & python both have:

'//'.split('/') => [ '', '', '' ] '/'.split('/') => [ '', '' ] '',split('/') => [ '' ]

so they indeed produce what parent expected.

To be fair, blocks, procs, and methods are fairly fundamental to Ruby. After a bit of study, they stop being surprising. I'm sure there are other examples, though.

I think the responses of "Well, it doesn't astonish me" and "principle least my surprise" point to something deep about how to write code for other humans:

As a field, we would benefit greatly from learning more cognitive science.

A core suggestion of John Osterhout's book A Philosophy of Software Design is to have well-named modules with 'thin' interfaces which accomplish a reasonably-deep set of responsibilities. But...why?

Working Memory -- Humans have limited Working Memory.

If you give a function:

- A clear name.

- Few simple parameters.

- No side-effects not in the name.

- No GOTOs or exceptions.

then a human can glance at it, put the name and parameters into working memory, and carry on.

But if you split up a namable responsibility across multiple files, then a human reading the codebase to understand data flow would need many more open editor windows or to hold much more info in working memory.

This is why I am a fan of long function names, like openUpgradeDowngradeSubscriptionDialog(). Even though they add a bit of verbosity to the code, I can immediately tell what they do without having to scroll to the function definition, and there is zero astonishment when they are called from elsewhere. To a lesser extent, same with variable names: speaking for myself (and perhaps only myself), I like long variable names -- as the saying goes, code is read many more times than it is written, so saving a few keystrokes just to shorten a variable name tends to carry a high cost that is paid down the line.

You're describing self-documenting code. Well written code shouldn't require any "what" comments, only "why" comments.

Too often you see code with a generically-named function/variable, and a comment next to it describing what it does. Why not just name the function/variable correctly?!

There's a strong cargo-cult idea that "real" programmers use short/abbreviated names, and yet there's absolutely no value to using "idx" over "index", or a more appropriately named "last_name_index".

Naming is always hard, but sometimes the "what" requires a full paragraph, and even very long variable names shouldn't exceed 80 characters all on their own.

torque_aux_motor_braking_only_safety_threshold_nm is a good variable name, but I wouldn't want to let them get much longer than that.

Totally agree. At least don’t use generic names like “Config”. Call it “”DatabaseConfig” that has some meaning.

In JavaScript, "0 < -1 < 2" evaluates to true.

The coercion rules were a big surprise to me when I first started to work in JS. Sometimes codes were obvious but the underlying interpretation is a surprise call.

0 < -1 < 2

To be fair, is there any mainstream language other than Python where that expression makes sense at all?

And even Python had to make explicit, syntactical support for it, and IMO it's a pretty non-obvious language hack. I mean, there's an invisible AND in there, that's pretty astonishing to me at least :-)

Works beautifully in Common Lisp. Of course, it is not mainstream.

? (< 0 1 2)


? (< 0 -1 2)


In Clojure:

   user=> (< 0 1 2)

   user=> (< 0 -1 2)

It's a big ask, to have a binary operator and ternary operator that use the same symbol.

It's honestly some fairly trivial AST-twiddling to support that feature. the compiler can simply detect cases where the left-hand-side of a comparison is /also/ a comparison and change the AST to make it two comparisons combined with an AND statement.

That said I find comparison chaining rather confusing and unexpected so I make a point of avoiding its use. Typing it out as two comparisons hardly saves any effort in any case.

I know, but it isn't a very useful feature, so why bother?

Reminds me of https://www.destroyallsoftware.com/talks/wat

Javascript is convenient for many use cases, but it definitely violates POLA on many levels, in many situations!

And it does in C as well!

So does this mean that JS does satisfy the principle of least astonishment for an experienced c-coder (something important when it was developed in the early 2000s)?

In what language (other than a RPN based language like Lisp) would this expect to return something meaningful?

Yes, you could say that. It preserves the order of operations and “reverse-truthiness” (i.e. boolean-to-integer) conversions from C.

There is one in Excel VBA (I assume the Behavior is still the same) where IIRC “Sort” called from code leaves its criteria in the dialog box and if those parameters are omitted on a second sort, the previous ones are used unless cleared. This can make previously working code stop working when someone works on an unrelated part of the app.

I once had an argument about usage of exceptions for flow control and I mentioned this principle.

The developer replied, mostly tongue in cheek to be fair:

Well, it doesn't astonish me

That's one of the arguments I used. We used C# and he actually did some noddy perf tests the conclusion of which where:

It's slower but only adds 0.01 s as compared to the a version not using exceptions. This was about 3 orders of magnitude slower (probably more under load would be my guess)

+1 for all the extra work to prove to himself that he had taken the right approach.

-10 For refusing to listen, ignoring data and cherry picking articles on C about exception handling (Of course they're not going to use exceptions, they don't exist)

That seems to me to be an implementation detail though.

There's no real reason you can't have a compiler that converts exception-throwing-code into error-code-returning-code. That'd basically cost you the performance of a couple if-statements per function call return, which doesn't seem too excessive to me.

Of course most languages don't implement exceptions this way and I'm sure there's a good reason for it but you can still choose -- as a language implementor -- to have cheap exceptions.

I think the time I’ve been most astonished recently by an api was asp.net. They map http parameters onto function parameters by name, using reflection. One of the first things you’ll read in any introductory programming textbook is that function parameter names don’t matter; only the type and order matters. When the light went off and I figured out what they had done, I swore out loud at my desk..

I often use this principle when explaining my code review objections: "I am slightly astonished that this code does X." If someone asks me if it's really astonishing, or what I mean by that, I explain the principle to them.

Do you find that it just pushes the argument "up the stack"? And now instead of the discussion being about the code you're arguing about which outcome would be less astonishing? It seems like it would move the conversation from concrete to abstract and subjective. If the person replies "this is how I would expect it to work" you can't really argue with that, because maybe that is what they'd expect, even if that's not what you think a generic person would expect.

Also, in my experience, one of the major holes that mediocre devs have is that they struggle to look at things from a different perspective (either the perspective of a user or the perspective of another developer). This principle is fully based on putting yourself in someone else's shoes and thinking about what they would expect, which is exactly what these devs struggle with.

I think that's the point, and the value of the discussion (hopefully not "argument").

In my imagined best case result, you and your coworker are now having a conversation about your backgrounds and perspectives to try to understand where the mismatch is that led to your astonishment. One possible outcome there is that you establish a new, common basis for understanding, and less future astonishment thereby.

To be honest the word “astonished” comes across a little weird in a code review. I think it’s better to say “I don’t understand” or ask “why is it this way”?”

> the word “astonished” comes across a little weird

... this is what gives me an opportunity to describe the principle :-)

and for those who know it - they already know what I mean.

I wish languages more clearly distinguished between functions that produce values and functions that have effects (and ones that do both, but discourage that).

I like how this ends up on the topic of efficient encoding of an x86 instruction that zeroes the accumulator.

xor reg, reg is the best because it's such an ancient idiom that processors recognize it as a zeroing idiom, enabling some processor-level optimizations.

Now you need to decide whether to use opcode 0x31 or 0x33…

Which one weighs less?

0x33 I think, by a minuscule amount, since it has fewer “on” bits?

Coding to impress your coworkers often results in a prioritization of most astonishment.

Same for CV-driven work: the adoption of modern "devops" tools is an endless bad surprise.

Perhaps one of the more important consequences of egoless programming.

Applications are open for YC Summer 2021

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact