[HN Gopher] The compiler is your best friend
       ___________________________________________________________________
        
       The compiler is your best friend
        
       Author : based2
       Score  : 121 points
       Date   : 2025-12-31 15:46 UTC (7 hours ago)
        
 (HTM) web link (blog.daniel-beskin.com)
 (TXT) w3m dump (blog.daniel-beskin.com)
        
       | LegionMammal978 wrote:
       | > How many times did you leave a comment on some branch of code
       | stating "this CANNOT happen" and thrown an exception? Did you
       | ever find yourself surprised when eventually it did happen? I
       | know I did, since then I at least add some logs even if I think
       | I'm sure that it really cannot happen.
       | 
       | I'm not sure what the author expects the program to do when
       | there's an internal logic error that has no known cause and no
       | definite recovery path. Further down the article, the author
       | suggests bubbling up the error with a result type, but you can
       | only bubble it up so far before you have to get rid of it one way
       | or another. Unless you bubble everything all the way to the top,
       | but then you've just reinvented unchecked exceptions.
       | 
       | At some level, the simplest thing to do is to give up and crash
       | if things are no longer sane. After all, there's no guarantee
       | that 'unreachable' recovery paths won't introduce further bugs or
       | vulnerabilities. Logging can typically be done just fine within a
       | top-level exception handler or panic handler in many languages.
        
         | skydhash wrote:
         | 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.
        
           | JohnFen wrote:
           | > 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.
        
             | skydhash wrote:
             | Such comments rot so rapidly that they're an antipattern.
             | Such assumptions are dangerous and I would point it out in
             | a PR.
        
               | LegionMammal978 wrote:
               | 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.
        
               | skydhash wrote:
               | 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.
        
               | tosapple wrote:
               | How does one defend against cosmic rays?
               | 
               | Keep two copies or three like RAID?
               | 
               | Edit: ECC ram helps for sure, but what else?
        
               | well_ackshually wrote:
               | >How does one defend against cosmic rays?
               | 
               | 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.
        
               | LegionMammal978 wrote:
               | > 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?
        
               | skydhash wrote:
               | 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.
        
               | LegionMammal978 wrote:
               | 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.
        
               | awesome_dude wrote:
               | Do you really have code that's
               | 
               | if array.Len > 2 { X = Y[1] }
               | 
               | For every CRUD to that array?
               | 
               | That seems... not ideal
        
               | skydhash wrote:
               | Yes. Unless there's some statement earlier that verify
               | that the array has 2 items. It's quick to do, so why not
               | do it?
        
             | addaon wrote:
             | But if you spell that `assert(false)` instead of as a
             | comment, the intent is equally clear, but the behavior when
             | you're wrong is well-defined.
        
               | eterm wrote:
               | Better yet, `assert(false, message)`, with the message
               | what you would have written in the comment.
        
               | addaon wrote:
               | `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.
        
               | eterm wrote:
               | 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.
        
               | breatheoften wrote:
               | what language are we talking about? If it's cpp then the
               | pronounciation depends on compiler flags (perhaps
               | inferred from CMAKE_BUILD_TYPE)
        
               | JohnFen wrote:
               | I agree that including that assert along with the comment
               | is much better. But the comment alone is better than
               | nothing, so isn't without value.
        
               | zffr wrote:
               | At least on iOS, asserts become no-ops on release builds
        
               | addaon wrote:
               | You can (and probably should) undef NDEBUG even for
               | release builds.
        
               | josephg wrote:
               | It really depends on the language you use. Personally I
               | like the way rust does this:
               | 
               | - assert!() (always checked),
               | 
               | - debug_assert!() (only run in debug builds)
               | 
               | - unreachable!() (panics)
               | 
               | - unsafe unreachable_unchecked() (tells the compiler it
               | can optimise assuming this is actually unreachable)
               | 
               | - if cfg!(debug_assertions) { ... } (Turns into
               | if(0){...} in release mode. There's also a macro variant
               | if you need debug code to be compiled out.)
               | 
               | This way you can decide on a case by case basis when your
               | asserts are worth keeping in release mode.
               | 
               | And it's worth noting, sometimes a well placed assert
               | before the start of a loop can improve performance thanks
               | to llvm.
        
               | addaon wrote:
               | > debug_assert!() (only run in debug builds)
               | 
               | 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.
        
               | fwip wrote:
               | I think you might have missed that they threw an
               | exception right under the comment.
        
             | dllthomas wrote:
             | 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.
        
           | AnimalMuppet wrote:
           | 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.)
        
             | skydhash wrote:
             | Not really. A quick git blame (or alternative) will give
             | you the required information about the validity of such
             | proof.
        
               | dullcrisp wrote:
               | You must have some git plugin I haven't heard about.
        
               | skydhash wrote:
               | 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.
        
               | AnimalMuppet wrote:
               | But you need the snippet and, potentially, the entire
               | call tree (both up and down).
        
               | dullcrisp wrote:
               | 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.
        
           | threethirtytwo wrote:
           | 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.
        
         | GabrielBRAA wrote:
         | Heh, recently I had to fix a bug in some code that had one of
         | these comments. Feels like a sign of bad code or laziness. Why
         | make a path that should not happen? I can get it when it's on
         | some while loop that should find something to return, but on a
         | if else sequence it feels really wrong.
        
           | kccqzy wrote:
           | Strong disagree about laziness. If the dev is lazy they will
           | not make a path for it. When they are not lazy they actually
           | make a path and write a comment explaining why they think
           | this is unreachable. Taking the time to write a comment is
           | not a sign of laziness. It's the complete opposite. You can
           | debate whether the comment is detailed enough to convey why
           | the dev thinks it's unreachable, but it's infinitely better
           | than no comment and leaving the unreachability in their head.
        
             | zephen wrote:
             | Laziness might or might not be involved in either path.
             | 
             | A stupid developer might not even contemplate that
             | something could happen.
             | 
             | A smarter developer might contemplate that possibility, but
             | discount it, by adding the path and comment.
             | 
             | Why is it discounted? Probably just priorities, more
             | pressing things to do.
        
         | the__alchemist wrote:
         | This is what rust's `unreachable()!` is for... and I feel
         | hubris whenever I use it.
        
           | tialaramex wrote:
           | You should prefer to write unreachable!("because ...") to
           | explain to some future maintenance engineer (maybe yourself)
           | why you believed this would never be reached. Since they know
           | it _was_ reached they can compare what you believed against
           | their observed facts and likely make better decisions.
           | 
           | But at least telling people that the programmer believed this
           | could never happen short-circuits their investigation
           | considerably.
        
         | thatoneengineer wrote:
         | Ideally, if you can convince yourself something cannot happen,
         | you can also convince the compiler, and get rid of the branch
         | entirely by expressing the predicate as part of the type (or a
         | function on the type, etc.)
         | 
         | Language support for that varies. Rust is great, but not
         | perfect. Typescript is surprisingly good in many cases. Enums
         | and algebraic type systems are your friend. It'll never be 100%
         | but it sure helps fill a lot of holes in the swiss cheese.
         | 
         | Because there's no such thing as a purely internal error in a
         | well-constructed program. Every "logic error" has to bottom out
         | in data from outside the code eventually-- otherwise it could
         | be refactored to be static. Client input is wrong? Error the
         | request! Config doesn't parse? Better specify defaults! Network
         | call fails? Yeah, you should have a plan for that.
        
           | josephg wrote:
           | Yes, this has been my experience too! Another tool in the
           | toolbox is property / fuzz testing. Especially for data
           | structures, and anything that looks like a state machine. My
           | typical setup is this:
           | 
           | 1. Make a list of invariants. (Eg if Foo is set, bar + zot
           | must be less than 10)
           | 
           | 2. Make a check() function which validates all the invariants
           | you can think of. It's ok if this function is slow.
           | 
           | 3. Make a function which takes in a random seed. It
           | initializes your object and then, in a loop, calls random
           | mutation functions (using a seeded RNG) and then calls
           | check(). 100 iterations is usually a good number.
           | 
           | 4. Call this in an outer loop, trying lots of seeds.
           | 
           | 5. If anything fails, print out the failing seed number and
           | crash. This provides a reproducible test so you can go in and
           | figure out what went wrong.
           | 
           | If I had a penny for every bug I've found doing this, I'd be
           | a rich man. It's a wildly effective technique.
        
           | dmurray wrote:
           | Not every piece of logic lends itself to being expressed in
           | the type system.
           | 
           | Let's say you're implementing a sorting algorithm. After step
           | X you can be certain that the values at locations A, B, and C
           | are sorted such that A <= B <= C. You can be certain of that
           | because you read the algorithm in a prestigious journal, or
           | better, you read it in Knuth and you know someone else would
           | have caught the bug if it was there. You're a diligent reader
           | and you've convinced yourself of its correctness, working
           | through it with pencil and paper. Still, even Knuth has bugs
           | and perhaps you made a mistake in your implementation. It's
           | nice to add an assertion that at the very least reminds
           | readers of the invariant.
           | 
           | Perhaps some Haskeller will pipe up and tell me that any type
           | system worth using can comfortably describe this
           | PartiallySortedList<A, B, C>. But most people have to use
           | systems where encoding that in the type system would, at
           | best, make the code significantly less expressive.
        
           | wakawaka28 wrote:
           | Sometimes the "error" is more like, "this is a case that
           | logically could happen but I'm not going to handle it, nor
           | refactor the whole program to stop it from being expressable"
        
         | svantana wrote:
         | > At some level, the simplest thing to do is to give up and
         | crash if things are no longer sane.
         | 
         | The problem with this attitude (that many of my co-workers
         | espouse) is that it can have serious consequences for both the
         | user and your business.
         | 
         | - The user may have unsaved data - Your software may gain a
         | reputation of being crash-prone
         | 
         | If a valid alternative is to halt normal operations and present
         | an alert box to the user saying "internal error 573 occurred.
         | please restart the app", then that is much preferred IMO.
        
           | Krssst wrote:
           | > Your software may gain a reputation of being crash-prone
           | 
           | Hopefully crashing on unexpected state rather than silently
           | running on invalid state leads to more bugs being found and
           | fixed during development and testing and less crash-prone
           | software.
        
           | Calavar wrote:
           | > If a valid alternative is to halt normal operations and
           | present an alert box to the user saying "internal error 573
           | occurred. please restart the app", then that is much
           | preferred IMO.
           | 
           | You can do this in your panic or terminate handler. It's
           | functionally the same error handling strategy, just with a
           | different veneer painted over the top.
        
         | CupricTea wrote:
         | >Further down the article, the author suggests bubbling up the
         | error with a result type, but you can only bubble it up so far
         | before you have to get rid of it one way or another. Unless you
         | bubble everything all the way to the top, but then you've just
         | reinvented unchecked exceptions.
         | 
         | Not necessarily. Result types are explicit and require the
         | function signature to be changed for them.
         | 
         | I would much prefer to see a call to foo()?; where it's
         | explicit that it may bubble up from here, instead of a call to
         | foo(); that may or may not throw an exception my way with no
         | way of knowing.
         | 
         | Rust is absolutely not perfect with this though since any
         | downstream function may panic!() without any indication from
         | its function signature that it could do so.
        
       | skydhash wrote:
       | 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.
        
         | xnorswap wrote:
         | I also don't like that phrasing. It's like complaining of
         | guard-rails while running around erratically.
         | 
         | The guard rails aren't abusing you, they're helping you. They
         | aren't "angry", they're just constraints.
        
           | scubbo wrote:
           | 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.
        
       | kridsdale1 wrote:
       | I really like modern Swift. It makes a lot of what this author is
       | complaining about, impossible.
       | 
       | The worst file I ever inherited to work on was the ObjC class for
       | Instagram's User Profile page. It looked like it'd been written
       | by a JavaScript fan. There were no types in the whole file,
       | everything was an 'id' (aka void*) and there were 'isKindOfClass'
       | and null checks all over the place. I wanted to quit when I saw
       | it. (I soon did).
        
         | glenjamin wrote:
         | Any advice on how to learn modern Swift?
         | 
         | When I tried to do learn some to put together a little app,
         | every search result for my questions was for a quick blog
         | seemingly aimed at iOS devs who didn't want to learn and just
         | wanted to copy-paste the answer - usually in the form of an
         | extension method
        
         | JackYoustra wrote:
         | Modern swift makes this technically possible but so cluttered
         | that it's effectively impossible, especially compared with
         | typescript.
         | 
         | Swift distinguishes between inclusive and exclusive /
         | exhaustive unions with enum vs protocols and provides no easy
         | or simple way to bridge between the two. If you want to define
         | something that typescript provides as easy as the vertical bar,
         | you have to write an enum definition, a protocol bridge with a
         | type identifier, a necessarily unchecked cast back (even if you
         | can logically prove that the type enum has a 1:1 mapping), and
         | loads of unnecessary forwarding code. You can try and elide
         | some of it with (iirc, its been a couple years)
         | @dynamicMemberLookup, but the compiler often chokes on this, it
         | kills autocomplete, and it explodes compile times because
         | Swift's type checker degrades to exponential far more
         | frequently than other languages, especially when used in
         | practice, such as in SwiftUI.
        
           | tizio13 wrote:
           | I think you're conflating 'conciseness' with 'correctness.'
           | The 'clutter' you're describing in Swift like, having to
           | explicitly define an Enum instead of using a vertical bar |,
           | is exactly what makes it more robust than TS for large-scale
           | systems.
           | 
           | In TypeScript a union like string | number is structural and
           | convenient, but it lacks semantic meaning. In Swift, by
           | defining an Enum, you give those states a name and a purpose.
           | This forces you to handle cases exhaustively and
           | intentionally. When you're dealing with a massive codebase
           | 'easy' type bridging is often how you end up back in 'id' or
           | 'any' hell. Swift's compiler yelling at you is usually it
           | trying to tell you that your logic is too ambiguous to be
           | safely compiled which, in a safety first language, is the
           | compiler doing its job.
        
             | llmslave2 wrote:
             | This is just cope. Swift's compiler doesn't choke because
             | your logic is too ambiguous, it chokes because it's simply
             | too slow at inferring types in certain cases, including a
             | very common case where you write a normal Swift UI view.
             | There is nothing ambiguous about that.
             | 
             | Secondly, I'm not sure why you think the mountains of
             | boilerplate to replace | leads to semantic meaning. The
             | type identifier itself is sufficient.
        
             | JackYoustra wrote:
             | I mean there should be _some_ sugar in a language like
             | swift, no? It has sugar in many parts of the language,
             | being able to define a simple type union should not require
             | custom declaration of every forwarding accessor, a
             | protocol, an enum, bridging back and forth between them,
             | etc.
             | 
             | It would be one thing to say that the vertical bar is a
             | shortcut for these more exhaustive constructs (setting
             | aside whether these constructs are actually exhaustive -
             | they're not really for the purposes they're used for in
             | practice), but as it is right now, you have no bar! If it
             | were simple and easy to use you'd have enums dictate the
             | state of every app which, as any app developer knows, is
             | not how its currently done! Swift enums are so hard to use
             | you have just a mess of optionals on view state instead of
             | how it should formally be done, where almost every non-
             | trivial view has a single state that's an enum of all
             | possible configurations of valid states.
             | 
             | Indeed, if you put in a ton of effort to try and go down
             | this route defining views "properly" with something like
             | Swift TCA, you're not going to be able to get there even if
             | you break up your app into 40 different targets because
             | incremental recompilation will take a few minutes for
             | something as simple as a color change.
        
       | mock-possum wrote:
       | Hello baader meinhoff my old friend - while I'm familiar with the
       | convention, I was just introduced formally to the phrase
       | "functional core, imperative shell" the other day, and now here
       | it is again.
       | 
       | "Learn to stop worrying and love the bomb" was definitely a
       | process I had to go through moving from JavaScript to Typescript,
       | but I do mostly agree with the author here wrt convention. Some
       | things, like using type names as additional levels of context -
       | UserUUID and ItemUUID each alias UUID, which in turn is just an
       | alias for String - have occurred to me naturally, even.
        
       | barishnamazov wrote:
       | The "lies" described here are essentially the definition of
       | weakly typed programming, even in statically typed languages.
       | 
       | Functional languages like ML/Haskell/Lisp dialects has no lies
       | built in for decades, and it's good to see the mainstream
       | programming (Java, TS, C++, etc.) to catch up as well.
       | 
       | There are also cute benefits of having strong schemas for your
       | API as well -- for example, that endpoint becomes an MCP for LLMs
       | automatically.
        
       | moth-fuzz wrote:
       | I'm not a fan of the recent trend in software development,
       | started by the OOP craze but in the modern day largely driven by
       | Rust advocates, of noun-based programming, where type hierarchies
       | are the primary interface between the programmer and the code,
       | rather than the data or the instructions. It's just so...
       | dogmatic. Inexpressive. It ultimately feels to me like a barrier
       | between intention and reality, another abstraction. The type
       | system _is_ the program, rather than the program being the
       | program. But speaking of dogma, the author 's insistence that not
       | abiding by this noun-based programming model is a form of 'lying'
       | is quite the accusatory stretch of language... but I digress at
       | the notion that I might just be a hit dog hollering.
        
         | kccqzy wrote:
         | The kind of noun-based programming you don't like is great for
         | large teams and large code bases where there is an inherent
         | communication barrier based on the number of people involved.
         | (N choose 2 = N*(N-1)/2 so it grows quadratically.) Type
         | hierarchies need to be the primary interface between the
         | programmers and the code because it communicates invariants on
         | the data more precisely than words. It is dogmatic, because
         | that's the only way it could work for large teams.
         | 
         | When you are the only programmer, this matters way less. Just
         | do whatever based on your personal taste.
        
           | llmslave2 wrote:
           | That sounds eerily similar to the "OOP is for large teams"
           | defence which is simply not true.
           | 
           | On the contrary, this noun-based programming explodes with
           | complexity on large teams. Yes, interfaces are obviously
           | important, but when every single thing is its own type and
           | you try to solve problems with the type system leading to a
           | combinatoric explosion of types and their interactions, what
           | do you think happens when you scale the team up?
        
         | llmslave2 wrote:
         | Agreed. It's often accompanied by the dogma "make invalid
         | states unrepresentable" which sounds good until you start
         | trying to encode into the type system foo.bar being 1-42 unless
         | foo.baz is above 10, where now foo.bar can be -42-1 instead,
         | but if foo.omfg is prefixed with "wtf" then foo.baz needs to be
         | above 20 for its modifiers to kick in.
         | 
         | Yeah good luck doing that in the type system in a way that is
         | maintainable, open to modification, an scales with complexity.
        
       | smj-edison wrote:
       | > Rust makes it possible to safely manage memory without using a
       | garbage collector, probably one of the biggest pain points of
       | using low-level languages like C and C++. It boils down to the
       | fact that many of the common memory issues that we can
       | experience, things like dangling pointers, double freeing memory,
       | and data races, all stem from the same thing: uncontrolled
       | sharing of mutable state.
       | 
       | Minor nit: this should be mutable state _and_ lifetimes. I worked
       | with Rust for two years before recently working with Zig, and I
       | have to say opt-in explicit lifetimes without XOR mutability
       | requirements would be a nice combo.
        
       | doug_durham wrote:
       | Typing is great, presuming that the developer did a thorough job
       | of defining their type system. If they get the model wrong, or it
       | is incomplete then you aren't really gaining much out of a
       | strictly typed language. Every change is a fight. You are likely
       | to hack the model to make the code compile. There is a reason
       | that Rust is most successful at low level code. This is where the
       | models are concrete and simple to create. As you move up the
       | stack, complexity increases and the ability to create a coherent
       | model goes beyond human abilities. That's why coding isn't math
       | or religion. Different languages and approaches for different
       | domains.
        
       | onionisafruit wrote:
       | He mentions this is from a podcast. Anybody know what podcast? It
       | seems like something I might like to listen to.
        
       | ZebusJesus wrote:
       | This was a great breakdown and very well written. I think you
       | made one of the better arguments for rust Ive read on the
       | internet but you also made sure to acknowledge that large code
       | bases are just a different beast all together. Personally I will
       | say that AI has made making code proofs or "formal verification"
       | more accessible. Actually writing a proof for your code or code
       | verification is very hard to do for most programmers which is why
       | it is not done by most programmers, but AI is making it
       | accessible and with formal verification of code you prevent so
       | many problems. It will be interesting to see where programming
       | and compliers go when "formal verification" becomes normal.
        
       | jez wrote:
       | For another perspective on "lying to the compiler," I enjoyed the
       | section on Loopholes in Niklaus Wirth's "Good Ideas, Through the
       | Looking Glass"[1]. An excerpt:
       | 
       |  _Experience showed that normal users will not shy away from
       | using the loophole, but rather enthusiastically grab on to it as
       | a wonderful feature that they use wherever possible. This is
       | particularly so if manuals caution against its use._
       | 
       |  _[...]_
       | 
       |  _The presence of a loophole facility usually points to a
       | deficiency in the language proper, revealing that certain things
       | could not be expressed._
       | 
       | Wirth's use of loophole most closely aligns with the unchecked
       | casts that the article uses. I don't think exceptions amount to
       | lying to the compiler. They amount more to assuming for sake of
       | contradiction, which is not quite lying (e.g., AFSOC is a valid
       | proof technique, but proofs can be wrong). Null as a form of
       | lying is not the fault of the programmer, that's more the fault
       | of the language, so again doesn't feel like lying.
       | 
       | [1] https://people.inf.ethz.ch/wirth/Articles/GoodIdeas.pdf
        
       | shevy-java wrote:
       | So his view is from a programmer / developer. That's fine.
       | 
       | I had an issue on my local computer system yesterday; manjaro
       | would not boot with a new kernel I compiled from source. It would
       | freeze, at the boot menu, which I never had before. Anyway. I
       | installed linuxmint today and went on to actually compile a
       | multitude of things from source. I finally finished compiling
       | mesa, xorg-server, ffmpeg, mpv, gtk3 + gtk4 - and the prior
       | dependencies (llvm etc...). So I am almost finished finally.
       | 
       | I had to invest quite a lot of time hunting for dependencies.
       | Most recent one was glad2 for libplacebo. Turns out "pip install
       | glad2" suffices here. But getting that wasn't so trivial. The
       | project project at pip website was virtually useless;
       | respectively I installed "pip install glad" which was too old.
       | Also took me perhaps one full minute or more to realise it.
       | 
       | I am tapping into LFS and BLFS webpage (Linux from scratch),
       | which helps a lot but it is not perfect. So much information is
       | not described and people have to know what they are doing. You
       | can say this is fair, as this is more for advanced users. Ok. The
       | problem is ... so many things that compilers do, is not well-
       | described; or at the least you can not easily find high quality
       | documentation. Google search is almost virtually useless now; AI
       | just hallucinates and flat out lies to you often. Or tells you
       | things that are trivia and you already know it. We kind of lose
       | quality here. It's as if everything got dumbed down.
       | 
       | Meanwhile more and more software is required to build other
       | software. Take mesa. Now I need not only LLVM but also the whole
       | spirv-stack. And shaderc. And lots more. And also rust - why is
       | rust suddenly such a huge dependency? Why is there such a
       | proliferation of programming languages? Ok, perhaps C and C++ are
       | no longer the best language, but WHY is the whole stack
       | constantly expanding?
       | 
       | We worship complexity. The compilers also become bigger and
       | bigger.
       | 
       | About two days ago I cloned gcc from https://github.com/gcc-
       | mirror/gcc. The .tar.xz sits at 3.8 GB. Granted, regular tarball
       | releases are much smaller, e. g. 15.1.0 tar.xz at 97MB (at
       | https://ftp.gnu.org/gnu/gcc/?C=M;O=D). But still. These things
       | become bigger and bigger. gcc-7.2.0.tar.xz from 9 years ago had a
       | size of 59M. Almost twice the size now in less than 10 years. And
       | that's really just like all the other software too. We ended up
       | worshipping more and more bloat. Nobody cares about size. Now one
       | can say "this is just static code", but this is expanded and it
       | just keeps on getting bigger. Look at LLVM. How to compile this
       | beast:
       | https://www.linuxfromscratch.org/blfs/view/svn/general/llvm.... -
       | and this will only get bigger and bigger and bigger.
       | 
       | So, back to the "are compilers your best friend"? I am not sure.
       | We seem to have the problem of more and more complexity getting
       | in at the same time. And everyone seems to think this is no
       | issue. I believe there are issues. Take slackware; basically it
       | was a one person maintains it. This may not be the primary
       | reason, but slackware slowed down a lot in the last some years.
       | Perhaps maintaining all of that requires a team of people. Older
       | engineers cared about size due to constraints. Now that the
       | constraints are less important, bloat became the default.
        
       | WalterBright wrote:
       | > How many times did you leave a comment on some branch of code
       | stating "this CANNOT happen" and thrown an exception?
       | 
       | My code is peppered with `assert(0)` for cases that should never
       | happen. When they trip, then I figure out why it happened and fix
       | it.
       | 
       | This is basic programming technique.
        
       | supermdguy wrote:
       | > A common pattern would be to separate pure business logic from
       | data fetching/writing. So instead of intertwining database calls
       | with computation, you split into three separate phases: fetch,
       | compute, store (a tiny ETL). First fetch all the data you need
       | from a database, then you pass it to a (pure) function that
       | produces some output, then pass the output of the pure function
       | to a store procedure.
       | 
       | Does anyone have any good resources on how to get better at doing
       | "functional core imperative shell" style design? I've heard a lot
       | about it, contrived examples make it seem like something I'd
       | want, but I often find it's much more difficult in real-world
       | cases.
       | 
       | Random example from my codebase: I have a function that
       | periodically sends out reminders for usage-based billing
       | customers. It pulls customer metadata, checks the customer type,
       | and then based on that it computes their latest usage charges,
       | and then based on that it may trigger automatic balance top-ups
       | or subscription overage emails (again, depending on the customer
       | type). The code feels very messy and procedural, with business
       | logic mixed with side effects, but I'm not sure where a natural
       | separation point would be -- there's no way to "fetch all the
       | data" up front.
        
         | brickers wrote:
         | This stuff is quite new to me as I've been learning F#, so take
         | this with a pinch of salt. Some of the things you'd want are: -
         | a function to produce a list of customers
         | 
         | - a function or two to retrieve the data, which would be passed
         | into the customer list function. This allows the customer list
         | function to be independent of the data retrieval. This is
         | essentially functional dependency injection
         | 
         | - a function to take a list of customers and return a list of
         | effects: things that should happen
         | 
         | - this is where I wave my hands as I'm not sure of the
         | plumbing. But the final part is something that takes the list
         | of effects and does something with them
         | 
         | With the above you have a core that is ignorant of where its
         | inputs come from and how its effects are achieved - it's very
         | much a pure domain model, with the messy interfaces with the
         | outside world kept at the edges
        
         | grayhatter wrote:
         | > there's no way to "fetch all the data" up front.
         | 
         | this is incorrect
         | 
         | I assume there's more nuance and complexity as for why it feels
         | like there's no way. Probably involving larger design decisions
         | that feel difficult to unwind. But data collection, decisions,
         | and actions can all be separated without much difficulty with
         | some intent to do so.
         | 
         | I would suggest caution, before implementating this directly:
         | but imagine a subroutine that all it did was lock some database
         | table, read the current list of pending top up charges
         | required, issue the charge, update the row, and unlock the
         | table. An entirely different subroutine wouldn't need to
         | concern itself with anything other than data collection, and
         | calculating deltas, it has no idea if a customer will be
         | charged, all it does is calculate a reasonable amount.
         | Something smart wouldn't run for deactivated/expiring accounts,
         | but why does this need to be smart? It's not going to charge
         | anything, it's just updating the price, that hypothetically
         | might be used later based on data/logic that's irrelevant to
         | the price calculation.
         | 
         | Once any complexity got involved, this is closer to how I would
         | want to implement it, because this also gives you a clear
         | transcript about which actions happened why. I would want to be
         | able to inspect the metadata around each decision to make a
         | charge.
        
           | supermdguy wrote:
           | That's a good point, thinking about it some more, I think the
           | business logic feels so trivial that it would make the code
           | harder to reason about if it were separated from the effects.
           | Currently, I have one giant function that pulls data, filters
           | it, conditionally pulls more data, and then maybe has one
           | line of effectful code.
           | 
           | I could have one function that pulls the wallet balance for
           | all users, and then passes it to a pure function that returns
           | an object with flags for each user indicating what action to
           | take. Then another function would execute the effects based
           | on the returned flags (kind of like the example you gave of
           | processing a pending charges table).
           | 
           | The value of that level of abstraction is less clear though.
           | Maybe better testability? But it's hard to justify what would
           | essentially be tripling the lines of code (one function to
           | pull the data, one pure function to compute actions, one
           | function to execute actions).
           | 
           | Additionally, there's a performance cost to pulling all
           | relevant data, instead of being able to progressively filter
           | the data in different ways depending on partial results
           | (example: computing charges for all users at once and then
           | passing it to a pure function that only bills customers whose
           | billing date is today).
           | 
           | Would be great to see some more complex examples of
           | "functional core imperative shell" to see what it looks like
           | in real-world applications, since I'm guessing the
           | refactoring I have in my head is a naive way to do it.
        
             | grayhatter wrote:
             | > The value of that level of abstraction is less clear
             | though. Maybe better testability?
             | 
             | You wouldn't do it to make it easier to test; you would do
             | it to make it easier to reason about. E.g. There's some bug
             | where some users aren't getting charged. You already know
             | where the bug is, or rather, you know it's not in the code
             | that calculates what the price would be. But now, as a
             | bonus, you also can freely modify the code that collects
             | the people to charge, and don't have to worry if modifying
             | that code will change how much other people get charged,
             | (because these two code blocks can't interact with each
             | other).
             | 
             | You know the joke/meme, 99 bugs in the code, take one down,
             | patch it around, 104 bugs in the code? Yeah, that's talking
             | about code like you're describing where everything is in
             | one function, and everything depends on everything else as
             | an intractable web somehow.
             | 
             | > But it's hard to justify what would essentially be
             | tripling the lines of code (one function to pull the data,
             | one pure function to compute actions, one function to
             | execute actions).
             | 
             | This sounds like you're charging per line of source code.
             | Not all code is equal. If you have 3x the amount of code,
             | but it's written in a way that turns something difficult,
             | or complex to understand and reason about, into something
             | trivial to reason about, what you have is strictly better
             | code.
             | 
             | The other examples or counter points you mention are merely
             | implementation details, that only make sense in the context
             | of your specific example/code base that I haven't read. So
             | I'm gonna skip trying to reasoning about the solutions to
             | them given the point of the style recommendations is to
             | write code in a way that is 1) easier to reason about, or
             | 2) impossible to get wrong ...but those really are the same
             | thing
        
         | pdmccormick wrote:
         | Conceptually, can you break your processing up into a more or
         | less "pure" functional core, surrounded by some gooey,
         | imperative, state-dependent input loading and output effecting
         | stages? For each processing stage, implement functions of well-
         | defined inputs and outputs, with any global side effects
         | clearly stated (i.e. updating a customer record, sending an
         | email) Then factor all the imperative-ish querying (that is to
         | say, anything dependent on external state such as is stored in
         | a database) to the earlier phases, recognizing that some of the
         | querying is going to be data-dependent ("if customer type X,
         | fetch the limits for type X accounts"). The output of these
         | phases should be a sequence of intermediate records that
         | contain all the necessary data to drive the subsequent ones.
         | 
         | Whenever there is an action decision point ("we will be sending
         | an email to this customer"), instead of actually performing
         | that step right then and there, emit a kind of deferred-intent
         | action data object, e.g. "OverageEmailData(customerID, email,
         | name, usage, limits)". Finally, the later phases are also
         | highly imperative, and actually perform the intended actions
         | that have global visibility and mutate state in durable data
         | stores.
         | 
         | You will need to consider some transactional semantics, such
         | as, what if the customer records change during the course of
         | running this process? Or, what if my process fails half-way
         | through sending customer emails? It is helpful if your queries
         | can be point-in-time based, as in "query customer usage as-of
         | the start time for this overall process". That way you can
         | update your process, re-run it with the same inputs as of the
         | last time you ran it, and see what your updates changed in
         | terms of the output.
         | 
         | If those initial querying phases take a long time to run
         | because they are computationally or database query heavy, then
         | during your development, run those once and dump the
         | intermediate output records. Then you can reload them to use as
         | inputs into an isolated later phase of the processing. Or you
         | can manually filter those intermediates down to a more useful
         | representative set (i.e. a small number of customers of each
         | type).
         | 
         | Also, its really helpful to track the stateful processing of
         | the action steps (i.e. for an email, track state as Queued,
         | Sending, Success, Fail). If you have a bug that only bites
         | during a later step in the processing, you can fix it and
         | resume from where you left off (or only re-run for the affected
         | failed actions). Also, by tracking the globally affecting
         | actions you can actually take the results of previous runs into
         | account during subsequent ones ("if we sent an overage email to
         | this customer within the past 7 days, skip sending another one
         | for now"). You now have a log of the stateful effects of your
         | processing, which you can also query ("how many overage emails
         | have been sent, and what numbers did they include?")
         | 
         | Good luck! Don't go overboard with functional purity, but just
         | remember, state mutations now can usually be turned into data
         | that can be applied later.
        
         | movpasd wrote:
         | If your required logic separates nicely into steps (like
         | "fetch, compute, store"), then a procedural interface makes
         | sense, because sequential and hierarchical control flow work
         | well with procedural programming.
         | 
         | But some requirements, like yours, require control flow to be
         | interwoven between multiple concerns. It's hard to do this
         | cleanly with procedural programming because where you want to
         | draw the module boundaries (e.g.: so as to separate logic and
         | infrastructure concerns) doesn't line up with the sequential or
         | hierarchical flow of the program. In that case you have to
         | bring in some more powerful tools. Usually it means
         | polymorphism. Depending on your language that might be using
         | interfaces, typeclasses, callbacks, or something more exotic.
         | But you pay for these more powerful tools! They are more
         | complex to set up and harder to understand than simple
         | straightforward procedural code.
         | 
         | In many cases judicious splitting of a "mixed-concern function"
         | might be enough and that should probably be the first option on
         | the list. But it's a tradeoff. For instance, you then could
         | lose cohesion and invariance properties (a logically singular
         | operation is now in multiple temporally coupled operations), or
         | pay for the extra complexity of all the data types that
         | interface between all the suboperations.
         | 
         | To give an example, in "classic" object-oriented Domain-Driven
         | Design approaches, you use the Repository pattern. The
         | Repository serves as the interface or hinge point between your
         | business logic and database logic. Now, like I said in the last
         | paragraph, you could instead design it so the business logic
         | returned its desired side-effects to the co-ordinating layer
         | and have it handle dispatching those to the database functions.
         | But if a single business logic operation naturally intertwines
         | multiple queries or other side-effectful operations then the
         | Repository can sometimes be simpler.
        
         | sltr wrote:
         | > Does anyone have any good resources on how to get better at
         | doing "functional core imperative shell" style design?
         | 
         | I can recommend Grokking Simplicity by Eric Normand.
         | https://www.manning.com/books/grokking-simplicity
        
       | joeriddles wrote:
       | Great read. C# has the concept of nullable reference types[1]
       | which requires you to be explicit if a variable can be null and
       | the compiler is aware of this. I would love to see a similar
       | feature in languages like TypeScript and Go.
       | 
       | [1]: https://learn.microsoft.com/en-us/dotnet/csharp/nullable-
       | ref...
        
         | armchairhacker wrote:
         | TypeScript has disctinct nullable and non-null types if you
         | enable `strictNullChecks` or `strict` in TSConfig
         | (https://www.typescriptlang.org/tsconfig/#strictNullChecks).
        
       | einpoklum wrote:
       | > _Rust makes it possible to safely manage memory without using a
       | garbage collector, probably one of the biggest pain points of
       | using low-level languages like C and C++._
       | 
       | In C++, memory management has not been a pain point for many
       | years, and you basically don't need to do it at all if you don't
       | want to. The standard library takes care of it well enough - with
       | owning containers and smart pointers.
       | 
       | > _And Rust is famous for its optimizations in the style of "zero
       | cost abstractions"._
       | 
       | No, it isn't that famous for those. The safety and no-UB
       | constraints prevent a lot of that.
       | 
       | By the way, C++, which is more famous for them, still struggles
       | in some cases. For example, ABI restrictions prevent passing
       | unique_ptr's via single registers, see:
       | https://stackoverflow.com/q/58339165/1593077
        
       | jibal wrote:
       | > I'm personally not aware of languages that completely remove
       | exceptions from the language.
       | 
       | Zig is one. For that matter standard C has no exceptions
        
       | zk108 wrote:
       | dragon book >> tiger book
        
       ___________________________________________________________________
       (page generated 2025-12-31 23:00 UTC)