>If you think you have even near impunity on social media, I have a bridge to sell you. Even a town to go with it.
I specifically said "near" impunity. If you do something bad enough they'll come after you but even then if your gripes are legitimate that's likely to amplify it.
Surely you're not honestly claiming that there is not a significant practical difference between modern internet criticism and the old ways when messaging that could reach the broad public was far thoroughly gated by people and things that had more stake in the power structure.
I wouldn’t say it was gated. More like it was costly. And people having the means to do so was a very small set and prone to agree with the status quo.
But even now, a lot of messages are lost on the internet. And the internet is only decentralized for messages propagation, not for access.
A comment "this CANNOT happen" has no value on itself. Unless you've formally verified the code (including its dependencies) and have the proof linked, such comments may as well be wishes and prayers.
Yes, sometimes, the compiler or the hardware have bugs that violate the premises you're operating on, but that's rare. But most non pure algorithms (side effects and external systems) have documented failure cases.
> A comment "this CANNOT happen" has no value on itself.
I think it does have some value: it makes clear an assumption the programmer made. I always appreciate it when I encounter comments that clarify assumptions made.
`assert(false)` is pronounced "this can never happen." It's reasonable to add a comment with /why/ this can never happen, but if that's all the comment would have said, a message adds no value.
Oh I agree, literally `assert(false, "This cannot happen")` is useless, but ensuring message is always there encourages something more like, `assert(false, "This implies the Foo is Barred, but we have the Qux to make sure it never is")`.
Ensuring a message encourages people to state the assumptions that are violated, rather than just asserting that their assumptions (which?) don't hold.
debug_assert!() (and it's equivalent in other languages, like C's assert with NDEBUG) is cursed. It states that you believe something to be true, but will take no automatic action if it is false; so you must implement the fallback behavior if your assumption is false manually (even if that fallback is just fallthrough). But you can't /test/ that fallback behavior in debug builds, which means you now need to run your test suite(s) in both debug and release build versions. While this is arguably a good habit anyway (although not as good a habit as just not having separate debug and release builds), deliberately diverging behavior between the two, and having tests that only work on one or the other, is pretty awful.
For example, I’m pretty sure some complex invariant holds. Checking it is expensive, and I don’t want to actually check the invariant every time this function runs in the final build. However, if that invariant were false, I’d certainly like to know that when I run my unit tests.
Using debug_assert is a way to do this. It also communicates to anyone reading the code what the invariants are.
If all I had was assert(), there’s a bunch of assertions I’d leave out of my code because they’re too expensive. debug_assert lets me put them in without paying the cost.
And yes, you should run unit tests in release mode too.
There is no recovery. When an invariant is violated, the system is in a corrupted state. Usually the only sensible thing to do is crash.
If there's a known bug in a program, you can try and write recovery code to work around it. But its almost always better to just fix the bug. Small, simple, correct programs are better than large, complex, buggy programs.
Correct. But how are you testing that you successfully crash in this case, instead of corrupting on-disk data stores or propagating bad data? That needs a test.
> Correct. But how are you testing that you successfully crash
In a language like rust, failed assertions panic. And panics generally aren't "caught".
> instead of corrupting on-disk data stores
If your code interacts with the filesystem or the network, you never know when a network cable will be cut or power will go out anyway. You're always going to need testing for inconvenient crashes.
IMO, the best way to do this is by stubbing out the filesystem and then using randomised testing to verify that no matter what the program does, it can still successfully open any written (or partially written) data. Its not easy to write tests like that, but if you actually want a reliable system they're worth their weight in gold.
I think the idea is that those asserts should never be hit in the first place, because the code is correct.
In reality, its a mistake to add too many asserts to your code. Certainly not so many that performance tanks. There's always a point where, after doing what you can to make your code correct, at runtime you gotta trust that you've done a good enough job and let the program run.
You don't. Assertions are assumptions. You don't explicitly write recovery paths for individual assumptions being wrong. Even if you wanted to, you probably wouldn't have a sensible recovery in the general case (what will you do when the enum that had 3 options suddenly comes in with a value 1000?).
I don't think any C programmer (where assert() is just debug_assert!() and there is no assert!()) is writing code like:
assert(arr_len > 5);
if (arr_len <= 5) {
// do something
}
They just assume that the assertion holds and hope that some thing would crash later and provide info for debugging if it didn't.
Anyone writing with a standard that requires 100% decision-point coverage will either not write that code (because NDEBUG is insane and assert should have useful semantics), or will have to both write and test that code.
>> A comment "this CANNOT happen" has no value on itself.
> I think it does have some value: it makes clear an assumption the programmer made.
To me, a comment such as the above is about the only acceptable time to either throw an exception (in languages which support that construct) or otherwise terminate execution (such as exiting the process). If further understanding of the problem domain identifies what was thought impossible to be rare or unlikely instead, then introducing use of a disjoint union type capable of producing either an error or the expected result is in order.
Most of the time, "this CANNOT happen" falls into the category of "it happens, but rarely" and is best addressed with types and verified by the compiler.
Importantly, specifying reasoning can have communicative value while falling very far short of formal verification. Personally, I also try to include a cross reference to the things that could allow "this" to happen were they to change.
Do you not make such a tacit assumption every time you index into an array (which in almost all languages throws an exception on bounds failure)? You always have to make assumptions that things stay consistent from one statement to the next, at least locally. Unless you use formal verification, but hardly anyone has the time and resources for that.
Decent code generally avoids indexing into arrays at all; if it does so then it does so in ways where the bound checks are so certain to succeed that you can usually explain it to the compiler (e.g. split an array into slices and access those slices).
that is what I thought you were saying, and it doesn't make much sense to me. AFAICT the point of arrays as a data structure is to allow relatively cheap indexing at the cost of more expensive resizing operations. What else would you do with an array other than index into it?
> e.g. split an array into slices and access those slices
How is this not indexing with a little abstraction? Aren't slices just a way of packaging an array with a length field it a standard way? I'm not aware of many array implementations without a length (and usually also capacity) field somewhere , so this seems like a mostly meaningless distinction (ie all sclices are arrays, right).
> AFAICT the point of arrays as a data structure is to allow relatively cheap indexing at the cost of more expensive resizing operations. What else would you do with an array other than index into it?
The main thing you do with arrays is bulk operations (e.g. multiply it by something), which doesn't require indexing into it. But yeah I think they're a fairly niche datastructure that shouldn't be privileged the way certain languages do.
> How is this not indexing with a little abstraction? Aren't slices just a way of packaging an array with a length field it a standard way?
Sure (well, offset and length rather than just a length) but the abstraction is safe whereas directly indexing into the array isn't.
If such an error happens, that would be a compiler bug. Why? Because I usually do checks against the length of the array or have it done as part of the standard functions like `map`. I don't write such assumptions unless I'm really sure about the statements, and even then I don't.
Unless you are in the extremely small minority of people who would actually be affected by it (in which case your company would already have bought ECC ram and made you work with three isolated processes that need to agree to proceed): you don't. You eat shit, crash and restart.
Well, bitflip errors are more of a vulnerability for longer lived values. This could effect fukushima style robots or even medical equipment. ECC implemented outside of ram would save vs triplicate but it was just a question related to the-above idea of an array access being assumed as in+bounds. Thank you.
> or have it done as part of the standard functions like `map`.
Which are all well and good when they are applicable, which is not always 100% of the time.
> Because I usually do checks against the length of the array
And what do you have your code do if such "checks" fail? Throw an assertion error? Which is my whole point, I'm advocating in favor of sanity-check exceptions.
Or does calling them "checks" instead of "assumptions" magically make them less brittle from surrounding code changes?
A comment have no semantic value to the code. Having code that check for stuff is different from writing comments as they are executed by the machine. Not read by other humans.
Of course you should put down a real assertion when you have a condition that can be cheaply checked (or even an assert(false) when the language syntax dictates an unreachable path). I'm not trying to argue against that, and I don't think anyone else here is either.
I was mainly responding to TFA, which states "How many times did you leave a comment on some branch of code stating 'this CANNOT happen' and thrown an exception" (emphasis mine), i.e., an assertion error alongside the comment. The author argues that you should use error values rather than exceptions. But for such sanity checks, there's typically no useful way to handle such an error value.
False it has value. It’s actually even better to log it or throw an exception. print(“this cannot happen.”)
If you see it you immediately know the class of error is purely a logic error the programmer made a programming mistake. Logging it makes it explicit your program has a logic bug.
What if you didn’t log it? Then at runtime you will have to deduce the error from symptoms. The log tells you explicitly what the error is.
Worse: You may created the proof. You may have linked to the proof. But if anyone has touched any of the code involved since then, it still has no value unless someone has re-done the proof and linked that. (Worse, it has negative value, because it can mislead.)
Git blame will show the commit and the date for each line. It’s easy to verify if the snippet has changed since the comment. i use Emacs and it’s builtin vc package that color code each block.
And anything that can affect relevant state, any dependencies that may have changed, validations to input that may have been modified; it’s hard to know without knowing what assumptions the assertion is based on.
The whole article gives a generated vibe, but I did want to point out this particular snippet
> The compiler is always angry. It's always yelling at us for no good reason. It's only happy when we surrender to it and do what it tells us to do. Why do we agree to such an abusive relationship?
Programming languages are a formal notation for the execution steps of a computing machine. A formal system is always built around rules and not following the rules is an error, in this case a malformed statement/expression. It's like writing: afjdla lkwcn oqbcn. Yes, they are characters, but they're not english words.
Apart from the syntax, which is a formal system on its own, the compiler may have additional rules (like a type system). And you can add even more rules with a static analysis tool (linter). Even though there may be false positives, failing one of those usually means that what you wrote is meaningless in some way. It may run, but it can have unexpected behavior.
Natural language have a lot of tolerance for ambiguous statements (which people may not be aware of if they share the same metaphor set). But a computer has none. You either follow the rules or you do not and have an error.
Right, and I suspect that was the author's intent - to evoke a sympathetic frustration that newer programmers might feel, and then to point out how the frustration is ill-aimed.
> Eventually you'll want to know what users are doing, and specifically why they're not doing what you expected them to do after you spent ages crafting the perfect user journeys around your app
That's putting the cart before the horse. The way it's properly done is just to invite a few users and measure and track their interaction with your software. And this way you'd have good feedback instead of frustrating your real users with slow software.
Yeah, you'll do that, and get great feedback, and then when you roll it out to other users they'll do weird stuff you've not seen any of the test group try before.
Users being weird are the fundamental root cause of all software problems. :)
Users can’t click a button that does not exist. It’s on product and engineering to curtail what the user can do. Optimizing for the happy path while not eliminating the incorrect flow is just bad software engineering.
I have spotify (The duo plan is cheap where I’m at and my SO wanted to use it work). But I do maintain my own collection, mostly because Spotify UI is pure garbage.
My collection is store on an old mac mini (with debian). It’s directly linked to an home theater setup via optical. I have MPD on it that I control with Rigelian on my phone. But I often just ssh and use MoC to play music.
I also have gonic (subsonic server) that uses the same library. I use Amperfy to play music from my phone. I could use navidrome, but I don’t like web players.
As for the management, it’s all manual. I’ve tried beets but the overhead wasn’t worth it. I have several collections which have different filesystem organization schemes.
I like go’s approach on having default value, which for struct is nil. I don’t think I’ve ever cared between null result and no result, as they’re semantically the same thing (what I’m looking for doesn’t exist)
And also false. Good programmers are always aware of the debt. It’s just not easily quantifiable as part of it can only be estimated when a change request has been made. And truly known when implementing the change.
It’s always a choice between taking more time today to reduce the cost of changes in the future, or get result fast and be less flexible later. Experience is all about keeping the cost of changes constant over time.
We can both be aware of the debt and still get choked by it. It's what makes pollution such a great metaphor. We know we're creating it -- even try to mitigate it -- but it still keeps growing.
Tech debt grows only when you don't care. We have codebases that are old enough to drive where the debt is not growing at all. You got there by the code being a good representation of the domain requirements in the technical space. If the domain don't change much (they rarely do), you don't have to do much work in the technical space. And if the latter change (platform and library updates), it's gradual enough that you can spread the cost over time.
You got tech debt when rushing to implement stuff while having an incomplete representation of the problem. And then trying to patch the wrong solution instead of correcting it.
If you think you have even near impunity on social media, I have a bridge to sell you. Even a town to go with it.
reply