[HN Gopher] The problem with invariants is that they change over...
       ___________________________________________________________________
        
       The problem with invariants is that they change over time
        
       Author : kiyanwang
       Score  : 112 points
       Date   : 2024-04-22 08:07 UTC (1 days ago)
        
 (HTM) web link (surfingcomplexity.blog)
 (TXT) w3m dump (surfingcomplexity.blog)
        
       | kibwen wrote:
       | I'm happy to see people talking about the problem posed by
       | implicit invariants, but this post strikes me as defeatist. This
       | line especially:
       | 
       |  _> Implicit invariants are, by definition, impossible to enforce
       | explicitly._
       | 
       | Yes, Hyrum's Law (and, at the limit, Rice's Theorem) and all that
       | means that we won't ever be perfect, but just because an
       | invariant is currently implicit doesn't mean it must remain
       | implicit forever. We can identify classes of implicit invariants
       | and make them explicit using things like type systems.
        
         | metalrain wrote:
         | I understood article saying that implicit invariants are like
         | undocumented choices.
         | 
         | If I have blog where blogposts have maximum length of 4096
         | bytes. But why would there be such limit, is it creative
         | decision? is there technical limitation? was it just some "good
         | enough" number when blog was created?
         | 
         | I don't think type systems really can encode these reasons. You
         | see the constraints but not the reasoning.
        
           | chuckadams wrote:
           | type BlogPostThatFitsInSMS = BlogPost<FixedString<150>>
           | 
           | No one would write this type in the real world (and most type
           | systems can't encode it), but you see lower-level sorts of
           | reasoning all over the place in type systems.
        
       | throwaway74432 wrote:
       | Yes, it's called evolution. Software evolves as well as scales.
       | Scaling is architected growth, evolution is unarchitected growth.
       | (Sometimes scaling results in unarchitected growth, if the
       | architecture was not reasonable.) There are many patterns for
       | handling evolution, but they almost always involve a pattern
       | outside of the existing architecture (a super architecture) to
       | lean on for support. In my opinion, codifying and optimizing
       | these super architecture patterns is one of the highest goals in
       | software engineering because they allow for less error prone
       | evolutions.
        
         | larsrc wrote:
         | Thanks to Titus Winters for the phrase "Software engineering is
         | programming integrated over time". Handling evolution of
         | software is different from just writing it.
        
       | bruce511 wrote:
       | Looking back, I find it hard to offer concrete advice. So many of
       | the "things I know to be true" changed over time, while so many
       | of the "doing x to allow for future y" didn't lead to anything.
       | 
       | On the other hand, I have systems being worked on and still
       | shipping 20 years old because of fortuitous decisions in the 90s.
       | Most of those taken because of bad experiences with earlier
       | systems.
       | 
       | For example, I'm a big fan of surrogate keys and UUIDs, in
       | database design. Mostly because of experience where they weren't
       | used which caused problems later. (By contrast, performance costs
       | of using them become lower every year.)
       | 
       | Conversely deciding to build code libraries parallel to
       | application development means long-term gains in performance and
       | reliability.
       | 
       | It's easy to view all this in hindsight, -much- harder with
       | foresight.
       | 
       | Today I try and see everything through 2 rules.
       | 
       | 1) beware of "always" statements, especially especially in regard
       | to client requirements. IME "always" is used as a synonym for
       | "mostly" and "at the moment".
       | 
       | 2) choose paths now that keep options open later. Will this
       | system ever use distributed data? Probably not, but choose a
       | primary key that leaves that option open.
       | 
       | Lastly, and this applies to data design, remember "nothing is
       | universal". Using Social Security number as an ID is great, until
       | they outsource. Phone number is always 8 digits? Yeah right.
       | Nobody works night shift... you get the idea.
       | 
       | If in doubt, err on the side of "variant". You'll be right more
       | often than not.
        
         | abraae wrote:
         | > For example, I'm a big fan of surrogate keys and UUIDs, in
         | database design.
         | 
         | You would think this (using surrogate keys) would be such well
         | worn wisdom by now that discussions about it wouldn't be a
         | thing. But somehow new generations of developers, weaned on
         | nosql and on not using the god-given gifts of databases like
         | integrity constraints, seem to love bike shedding key design
         | and arguing vociferously that natural keys can be used as
         | primary (and foreign) keys.
        
           | ThereIsNoWorry wrote:
           | You wouldn't believe how often I have to fight for UUIDs
           | instead of sequencing. UUIDs are great. For all practical
           | purposes 0 possibility of collision; you can use it as an ID
           | in a global system, it just makes so much fucking sense.
           | 
           | But the default is still a number natural number sequence. As
           | if it matters for 99% of all cases that stuff is "ordered"
           | and "easily identifiable" by a natural number.
           | 
           | But then you want to merge something or make double use and
           | suddenly you have a huge problem that it isn't unique anymore
           | and you need more information to identify.
           | 
           | Guess what, an UUID does that job for you, across multiple
           | databases and distributed systems, a UUID is still unique
           | with 99.9999% probability.
           | 
           | The one counter-example every 10 years can be cared for
           | manually.
        
             | abraae wrote:
             | As far as I know the only practical downside with UUIDs in
             | the modern age - unless you like using keys for ordering by
             | creation time, or you have such enormous volumes that
             | storage is a consideration - is that they are cumbersome
             | for humans to read and compare, e.g scanning log files.
             | 
             | And in any case the German tank problem means that you
             | often can't use incrementing numbers as surrogate keys if
             | they are ever exposed to the public e.g in urls.
        
               | mrkeen wrote:
               | > they are cumbersome for humans to read and compare, e.g
               | scanning log files.
               | 
               | I think the positive still outweighs the negative here:
               | you can search your whole company's logs for a UUID and
               | you won't get false positives like you would with serial
               | integers.
        
             | akira2501 wrote:
             | I've been using UUIDv7 a lot lately and I've been quite
             | happy with the results.
        
           | prometheus76 wrote:
           | We were pulling the entire list of tasks (around 80,000 tasks
           | from various projects) from Microsoft Project 2012 (MSP) and
           | dumping it into a database every day, so that we could track
           | progress of projects daily. MSP uses GUIDs and they are great
           | for uniqueness, but the memory footprint they caused when
           | reporting using Power BI became untenable after about six
           | months.
           | 
           | We ended up creating our own integer table tied to the GUIDs
           | and replaced all the GUIDs with integers in our reporting
           | database. Our memory footprint dropped by 10X and we were
           | able to report on years worth of data, rather than months.
        
             | CodesInChaos wrote:
             | > Our memory footprint dropped by 10X
             | 
             | How? A UUID is only 4x the size of a 32-bit integer.
        
               | prometheus76 wrote:
               | We had multiple columns using UUID that we needed, and
               | the Tabular memory model goes up exponentially for every
               | column.
        
           | marcosdumay wrote:
           | I never found any kind of literature that clearly states that
           | artificial keys are superior identifiers than natural ones.
           | 
           | Every material about relational data I've seen either just
           | explains them, says they can be either natural or artificial,
           | and leaves at that, or says there's some disagreement over
           | what is better.
           | 
           | The unanimity exists only within field experts that apply the
           | stuff. The theoreticians all disagree.
        
             | CodesInChaos wrote:
             | I almost always use surrogate keys as primary keys (or a
             | tuple of them, for join-tables). Natural keys are rarely as
             | immutable and unique as we'd like.
             | 
             | Even ISO country codes can be problematic, for example we
             | currently use XK for Kosovo, but it might get an official
             | code at some point.
        
             | bruce511 wrote:
             | It's less about literature, and more about experience.
             | 
             | Over the years just about every natural key I've seen ends
             | up being either not-universal (-everyone- has a phone
             | number right? Even that new-born baby?) Or not unique
             | (phone numbers can be shared) or mutable (like people's
             | names changing).
             | 
             | It's just so much easier to add a column and use a
             | surrogate key. I prefer uuids because they are unique And
             | immutable - properties I really appreciate in primary keys.
        
               | abraae wrote:
               | It's an easy challenge. Just ask someone to name a
               | natural key that will never change. They are as rare as
               | rocking horse shit.
        
         | Terr_ wrote:
         | I like to say "design for deletion", meaning the priority is
         | making sure code with outdated assumptions or fit can be
         | _found, deleted, and replaced_ easily.
         | 
         | This is in contrast to my how my younger-self would instead
         | focus on making code that "extensible" or "flexible" or
         | "configurable". (With some overtones of impressing people,
         | leaving a long-term mark upon the project, etc.)
         | 
         | Nope! Go for "delete-able."
        
           | vsnf wrote:
           | I've never heard this framing before. Can you offer any
           | examples of what you mean?
        
             | Terr_ wrote:
             | A lot of it overlaps with Ya Ain't Gonna Need It and
             | avoiding strong coupling, but I think the framing makes it
             | easier to stay on target: A developer is less-likely to end
             | up going: "Hey guys, I created a _decoupling framework_ so
             | we can swap anything for anything whenever we want! "
             | 
             | If you design thinking "X years from now business-
             | requirements or staff will have changed enough that this
             | will need to be ripped out and rewritten, and I am not good
             | enough at predicting the future to solve it cleverly", then
             | it sets up a different expectation of how you invest your
             | time.
             | 
             | One might focus on code where it's easy to trace
             | dependencies, even if that means a bit more boilerplate and
             | avoiding layers of indirection that aren't (yet/ever)
             | needed. "Greppability" of code also becomes important,
             | especially if your language/tools aren't _very_ good at
             | tracing usages and caller /callee relationships.
        
               | whstl wrote:
               | Yep, you nailed it.
               | 
               | Often the problem is exactly this magic "decoupling
               | framework", which ends up being exactly the problematic
               | part that that one eventually wants to swap out.
        
           | hinkley wrote:
           | There's a lot of power in designing unit tests for deletion.
           | I'd have to think about whether I feel that applies
           | universally.
        
         | magicalhippo wrote:
         | > beware of "always" statements, especially especially in
         | regard to client requirements.
         | 
         | Along with its complimentary statement:
         | 
         | Beware of "never", especially in regards to client
         | requirements.
         | 
         | Been on too many projects where it turned out "never" meant
         | once or twice per year once we'd gone into production...
        
           | bluGill wrote:
           | I put it a different way: programmers should gain domain
           | knowledge instead of reading requirement only. If you have an
           | understanding of the customers you can make a good call 90%
           | of the time as to what customers will demand in the future
           | without marketing misunderstanding. Marketing often wants
           | todays features now and so will tell you "never" when what
           | they really mean is don't make the next release take longer
           | to add that future feature, but if you can design for it
           | without making the schedule take too much longer do it.
        
             | magicalhippo wrote:
             | Having domain knowledge is a superpower of sorts. We're a
             | small team doing B2B software, and especially us senior
             | programmers and the head of the programming team has a lot
             | of domain knowledge.
             | 
             | Thus as you say we can very often immediately figure out
             | when something is missing from the requirements, or when we
             | feel things might be changing soon down the line.
             | 
             | We can also detect when the customer is requesting
             | something suboptimal or similar. We've received a lot of
             | positive feedback from customers due to us devs pushing
             | back, helping them to improve their processes and workflow.
             | But it also leads to less work for us, or more generic code
             | which can be reused by more customers.
             | 
             | Our support team also has extensive domain knowledge, many
             | have been hired from our customers. It makes our support
             | excellent, often the customer's issue is a combination of
             | how to do X and how to do X in our program, and for new
             | devs this is a great resource to tap into.
             | 
             | Together we're punching well above our weight, dominating
             | our niche with a quite small team. And domain knowledge has
             | helped a lot in that.
        
         | xxs wrote:
         | >For example, I'm a big fan of surrogate keys and UUIDs, in
         | database design.
         | 
         | Why I fully support surrogate keys and versioning, I do not
         | support UUIDs as primary keys - they are trivial to generate,
         | not so much to place in B-trees. It's very common to
         | need/process recently added data together, UUID make that much
         | harder as they inherent placement is all over the place w/o any
         | locality.
         | 
         | UUIDs are ok/fine as external references, of course.
        
           | antonyt wrote:
           | This is a solved problem. See UUIDv6 and higher.
        
       | yen223 wrote:
       | The problem I think is that when you define an invariant, you are
       | also defining an infinite number of "implicit" invariants. And
       | it's hard to know a priori which of those implicit invariants is
       | not an actual invariant over time.
       | 
       | Take a simple User type                 type User {         name:
       | string         email: string       }
       | 
       | This describes some obvious invariants - that a User will have a
       | name and an email.
       | 
       | But it's also describing some less obvious invariants, e.g. that
       | a User will _not_ have a phone number. This has implications if a
       | phone number ever does get added to a User type, and your system
       | has to deal with two different versions of Users.
       | 
       | (This isn't just an academic problem - the problem of trying to
       | evolve fields over time is the reason why Protobufs got rid of
       | the idea of a "required" field.)
       | 
       | And this is an example of a very simple implicit invariant. In
       | practice, systems that interact with each other will give rise to
       | emergent behaviour that one might think is always true, but is
       | not guaranteed.
        
         | _ZeD_ wrote:
         | >>> (This isn't just an academic problem - the problem of
         | trying to evolve fields over time is the reason why Protobufs
         | got rid of the idea of a "required" field.)
         | 
         | if _this_ is the reason of the drop of the required field it
         | doesn 't seems smart to me.
         | 
         | They are conflating the definition of a scheme with the scheme
         | evolution logic.
         | 
         | And there are multiple ways to deal with the latter (like
         | adding a schema version, for instance)
        
           | yen223 wrote:
           | I don't have a very strong opinion about that change
           | specifically. I think they could have kept their cake and ate
           | it too by solving the schema evolvability some other way, but
           | I also suspect those other ways will come with tradeoffs that
           | are also not pleasant.
           | 
           | But, the point is schema evolvability is a real problem, and
           | it's often not one that a lot of engineers give a lot of
           | thought to, even those who live in very strict statically-
           | typed worlds.
        
           | larsrc wrote:
           | Some "required" fields in very commonly used protos ended up
           | being unused, but everybody has to fill them out all the
           | time. If they didn't, the proto wouldn't even parse, by
           | design.
           | 
           | Protobufs explicitly do not have a built-in version system
           | (though of course you could add an optional field for it),
           | presumably because it is better to inspect the data than
           | build in assumptions that a certain version overs a certain
           | invariant.
        
         | atoav wrote:
         | This is why I tend to like to use strong type systems where
         | possible and necessary.                   struct LoginUser {
         | name: UserName,             email: Email,
         | phone_number: Option<PhoneNumber>,         }
         | 
         | First the user type should reflect the function of the user.
         | This is one that is allowed to log in (of course overly
         | simplified). You can then also have a OnboardingUser, a
         | ArchivedUser and so on. And if you want to transition one
         | between those different types you need to ensure the fields are
         | there. This is easy to test and clear.
         | 
         | The other thing is to use types for fields that have hard
         | verification requirements. You can still have them behave
         | mostly like strings for reading, but you can ensure that an
         | Email in a LoginUser never contains an unverified Email, by
         | first verifying the email string in the OnboardingUser and only
         | converting it into the Email type once it is guaranteed to be a
         | valid email. Because you use a special type tacking on further
         | things like storing a date when this verification happened last
         | is easy, just put it into the type as well.
         | 
         | Lastly the phone number is an Optional type that wraps a
         | PhoneNumber type. This way it can either be None or
         | Some<PhoneNumber> (this uses the Rust typesystem, but the
         | principle would work elsewhere), the phone number should be
         | verified the same way, ideally relying on a well tested method
         | that also fixes the formatting at the same time.
         | 
         | Now in Rust if some part of the system didn't handle a user
         | that _has_ a phoneNumber your code won 't compile, because from
         | the standpoint of the old code this would now be an entirely
         | new, unknown type never seen before. Sure you could step
         | through your code and explicitly _not_ handle it on purpose
         | because you want back to a running system fast, fail to add a
         | TODO and then later forget about having to do it, but that
         | would be entirely on you.
         | 
         | Now those hard typesystem constraints become even more valuable
         | when dealing with external systems. Your type should represent
         | all assumptions you are making about the data and test those on
         | each import. Of course you then will have import failures if
         | the import source changed, but we _want_ that to fail if it
         | changes. What we don 't want is the thing silently chugging the
         | input into the wrong pipe or invalid phone numbers to be stored
         | because the source system decided to split them up into the
         | prefix and the rest.
        
           | yen223 wrote:
           | Static types are really good for describing invariants, I
           | definitely agree.
           | 
           | But it does also describe a lot of implicit invariants. The
           | LoginUser you've defined has the implicit invariant that a
           | LoginUser only has 3 fields. It is very possible for someone
           | to write a function that relies on a LoginUser only ever
           | having 3 fields, which then breaks if you ever extended
           | LoginUser. This is similar to the example that the article
           | mentions at the start, where a thing that someone assumed
           | would always fit in one memory region no longer did, and that
           | caused all kinds of problems.
           | 
           | Those kinds of implicit invariants are the ones that are
           | tricky to make explicit, even in strongly-typed languages.
        
             | saghm wrote:
             | > But it does also describe a lot of implicit invariants.
             | The LoginUser you've defined has the implicit invariant
             | that a LoginUser only has 3 fields. It is very possible for
             | someone to write a function that relies on a LoginUser only
             | ever having 3 fields, which then breaks if you ever
             | extended LoginUse
             | 
             | Rust does actually have a good way of dealing with this
             | specific invariant. If you mark a struct with the
             | `non_exhaustive` attribute[0], trying to construct it with
             | a literal will fail outside of the package that defines it
             | (even if all fields are marked as public), and trying to
             | pattern match it outside the package that defines it is
             | required to use the operator to ignore any missing fields.
             | 
             | Not trying to say that it's possible to encode every
             | possible invariant in Rust, but I do find it useful to
             | leverage all of the tools available, and I've found this
             | one super useful (on both structs and enums) despite not
             | being super well known from what I can tell.
             | 
             | [0]: https://doc.rust-
             | lang.org/reference/attributes/type_system.h...
        
             | valenterry wrote:
             | I think that's a wrong assumption on your side. You are
             | assuming that the type describes the "actual thing". But
             | there is no such actual thing really. All we do is model.
             | And here we model a _view_ of the user within a certain
             | context. Totally possible that a different department will
             | model _their_ user in a different way.
             | 
             | This isn't even a technical problem, it's a semantical
             | problem. Trying to reuse the same type throughout the whole
             | world is doomed to fail, no matter what techniques you use.
        
               | atoav wrote:
               | This is good advice. If you take your models of the world
               | too seriously chances are you are in the process of
               | programming yourself into a corner, as things get more
               | complex.
               | 
               | Getting attached to your classes by writing a single
               | superclass of The User(tm) is a sure way of missing out
               | on the benefits of the typesystem and create coupling
               | where there should be none.
               | 
               | Knowing where to couple things and where to decouple
               | things is one of those hard programming issues, but one
               | that might be even more important than naming things.
        
             | atoav wrote:
             | This was a top of my head example of how you fan catch many
             | invariants, not meant to be exhaustive perfect code. I was
             | also implying the user type is something you use within
             | your application, not something you expose to other people
             | directly. If your application has to expose internal data
             | it makes often more sense to add a specific interface for
             | it (e.g. an API with versions). If there is code where
             | people rely on your internal types, it is your
             | responsibility to use semantic versioning to communicate
             | breaking changes, while it is the other person's
             | responsibility to assume breaking changes when versions
             | change.
             | 
             | If we are talking about someone other relying on your user
             | to have 3 fields that is a collegue: it is a good idea to
             | test your code against the whole codebase. Then you know
             | ahead what will fail and can act accordingly.
        
           | cryptonector wrote:
           | Using "optional" is not really enough for struct type
           | extensions. You really want something more akin to ASN.1's
           | extensibility markers, or typed-holes, or class derivation,
           | etc.
        
         | cryptonector wrote:
         | > But it's also describing some less obvious invariants, e.g.
         | that a User will not have a phone number.
         | 
         | That's not right. For one, the `User` type might be extensible
         | (what language is this?). For another one can always add
         | additional metadata about users elsewhere -- think of how you
         | might do it in a relational database.
         | 
         | > (This isn't just an academic problem - the problem of trying
         | to evolve fields over time is the reason why Protobufs got rid
         | of the idea of a "required" field.)
         | 
         | Protobufs is the poster boy of bad reinvention. ASN.1 got all
         | these things right over a lot of time, and instead of taking
         | the best of it and leaving out the worst of it, everyone is
         | constantly reinventing ASN.1 badly. And yes, ASN.1's evolution
         | was "academic" in many ways, and its authors blazed trails.
         | ASN.1 never had a "required" anything, but it did (and does)
         | have an OPTIONAL keyword.
         | 
         | The reason I mention extensibility above is that ASN.1 has been
         | through the whole academic study of extensibility over several
         | decades:                 - since the original encoding rules
         | were         tag-length-value, in the beginning you
         | could just add fields as long as you         knew the recipient
         | could handle them         (e.g., by ignoring unknown fields)
         | - when that proved unsatisfying they         added
         | extensibility markers for         denoting "here go extensions"
         | vs         "no extensions allowed", and "these         are
         | version 2 extension fields",         and so on, and not just
         | fields but         also things like INTEGER ranges,
         | enums, etc.            - they also put a lot of effort into
         | formalizing the use of "typed holes"         (things like
         | {extension-type-ID,          <encoded-value-of-that-type>}),
         | and they did this pretty much before         any other schemes.
         | (The wikipedia page on comparing          serialization
         | schemes[0] refers to          "typed holes" as "references",
         | FYI.)
         | 
         | Extensibility is fairly well covered in many serialization
         | formats now, like XML for example, but still fairly poorly
         | covered in others.
         | 
         | In SQL and relational systems in general one can always add
         | additional fields via tables to JOIN on, and often by adding
         | columns to existing tables.
         | 
         | Extensibility _is_ a big deal, and serious systems will have
         | had serious thought put into it.                 [0]
         | https://en.wikipedia.org/wiki/Comparison_of_data-
         | serialization_formats
        
       | brabel wrote:
       | I have always used asserts to enforce invariants (sometimes not
       | just actual "assert" but things like `Preconditions` in Java
       | which are always enabled). Those will actually break during
       | testing if they ever change.
       | 
       | I understand you cannot assert all invariants, but as far as I
       | can see, that's your main tool against this problem.
        
         | perlgeek wrote:
         | And those that you cannot assert, you can still document, which
         | will make debugging easier.
        
       | sixthDot wrote:
       | Sure sometimes there are deprecations and API changes.
       | Assumptions must be revised accordingly.
        
       | BlackFly wrote:
       | I would recommend not calling such things invariants and not
       | thinking about them in the same way you think about invariants.
       | If you would allow a comparison to physics then you would call
       | such things assumptions: frictionless pulleys, small angle
       | deflections, much slower than light speed, less than nuclear
       | density. The physical theory developed is then correct while the
       | assumptions hold. If you are making such an assumption and your
       | language supports it, add a debug assertion if possible.
       | 
       | For me, invariants are constructs of the design of an algorithm.
       | A list doesn't change while iterating over it-by construction,
       | not because someone else owns the reference and promises not to
       | change it concurrently. This structure can only be instantiated
       | via this function which ensures that that string field can always
       | contains only numeric digits. Those are invariants, thing you
       | enforce by design so you can rely on that later. Assumptions
       | instead are things you do not enforce but rely on anyways.
       | 
       | Back to physics, an invariant would generally be something like
       | conservation of energy or the speed of light being universal.
       | Both of these things are only invariant in certain physical
       | theories which enforce them by construction.
        
         | DougBTX wrote:
         | Agreed, we shouldn't use "invariant" as a shorthand for "not
         | expected to change".
         | 
         | We can still talk about "implicit assumptions" and "explicit
         | assumptions", so the article still works after that
         | replacement. The headline isn't as punchy though.
        
         | richrichie wrote:
         | Well stated. Physics analogy is spot on.
        
         | n4r9 wrote:
         | Precisely. If you asked a developer at the time whether the
         | "assumption that any loaned memory would fit into one region"
         | was static, I'm guessing they'd say "no, it's just what we've
         | assumed for now. I'm hoping that no one changes that without it
         | being discussed properly."
        
         | Joel_Mckay wrote:
         | Personally, I view most invariants as another form of state
         | embedding. It can often be unrolled or duplicated (GPU mailbox)
         | for parallelization, but does not necessarily exclude out-of-
         | order chip specific atomic operations on semaphores etc.
         | 
         | There is a folk song written about Black flies:
         | 
         | https://www.youtube.com/watch?v=w2q8jEuH3nk
         | 
         | =3
        
         | MereInterest wrote:
         | I think I'd put the difference between "invariant" and
         | "assumption" as purely a difference in framing. Both describe a
         | problem in communication between two components of a system.
         | Suppose there are two components, where the output of component
         | A is sent to component B. Both components seem to be working
         | correctly, but when run together they are not producing the
         | expected output.
         | 
         | * Option 1: Component A is violating an invariant of its output
         | type. Whether that is invalid data (e.g. nullptr), internal
         | inconsistency (e.g. an index that will be out of bounds for its
         | array), or some (e.g. matrices with determinant one), the
         | output generated by Component A is wrong in some way.
         | 
         | * Option 2: Component B is making an unjustified assumption
         | about its input type. The implementation was only valid when
         | the assumption held, but Component B's interface implied that
         | it could accept a wider range of inputs.
         | 
         | The only difference between these two cases is the human-level
         | description of what the type is "supposed" to hold. By
         | describing it as an "invariant", the blame is placed at
         | Component A. By describing it as an "assumption", the blame is
         | placed at Component B.
        
         | im3w1l wrote:
         | Well consider a system built on the assumption of conservation
         | of energy. Then a few product iterations the system might start
         | leaking energy into the environment. Maybe it's the neodymium
         | magnet in some unrelated component leading to eddy current
         | braking?
        
           | epgui wrote:
           | That's not a violation of the conservation of energy, and the
           | invariant isn't broken.
        
         | xmcqdpt2 wrote:
         | I don't think the examples you provided differ all that much.
         | Someone might add a new constructor that no longer sets the
         | string fields correctly, or your function might get modified so
         | that it can be passed a mutable structure. Type systems and
         | tests can be used to make it harder for future people to break
         | your invariants, but they can still be broken.
        
           | jayd16 wrote:
           | Theres a difference between the user breaking an assumption
           | and a self inflicted breaking change. For starters the former
           | can happen after compile time.
        
       | cjfd wrote:
       | When I read this story it leaves me wondering if these people had
       | any automated testing. This may or may not have helped but it is
       | not even mentioned and it does sound like a problem that an
       | integration test might have caught.
        
         | dexwiz wrote:
         | Yeah this is the kind of thing testing is useful for. Writing
         | tests against invariants is a great idea. That way if the
         | invariant changes, the test starts to fail.
        
         | fl0ki wrote:
         | If every test assumes the same invariant, then no test ever
         | covers cases where the invariant doesn't hold.
         | 
         | I've seen mature teams get this wrong even for basic things.
         | For example, they had a factor option, but all of their tests
         | only used a factor of 1.0 -- applying the factor 0 or 2+ times
         | didn't fail any tests, the factor was effectively completely
         | untested despite "coverage".
         | 
         | My tests caught it because I was a dependent of their code and,
         | frankly, trusted them much less than they trusted themselves.
         | Rightly in this case, and it turned out, many others.
         | 
         | My philosophy was to test with exactly the configuration to be
         | used in production and as much as possible [representative
         | subsets of] the data to be used in production. Their philosophy
         | was only to test contrived isolated cases which proved grossly
         | inadequate with respect to production.
        
       | olivierduval wrote:
       | IMHO, an "invariant" is only relative to the current system to be
       | build, based on a specific set of specifications. As soon as you
       | build a new version of the system (either because you change the
       | functionalities, the implementation or because the environment
       | change), you're expected to check which invariant still applies
       | and which should be added, removed or changed...
       | 
       | In a way, "invariant" are the same as "tests sets": they are here
       | to help to ensure that the product will have some specific
       | properties. Both are used during the product development and
       | checked. And both must be updated when the specs change
        
       | tossandthrow wrote:
       | > That assumption became obsolete the moment that Matt
       | implemented task packing, but we didn't notice. This code, which
       | was still simple and easy to read, was now also wrong.
       | 
       | It seems like Matt did not fully understand the platform he was
       | developing for and introduced a bug as his code did not satisfy
       | the invariants.
       | 
       | It is really nice when invariants are checked either in types or
       | in tests so Matt would have been alerted when he introduced the
       | bug.
       | 
       | I don't like the discourse the article introduces. It must always
       | be the one who writes the newest code who has the responsibility
       | to adjust previous code. That also includes changing obselete
       | modules (where invariants does no suffice anymore).
        
         | xmcqdpt2 wrote:
         | I work on a library used by millions of LOC in a monorepo. For
         | some changes, it is literally impossible to know all the places
         | that could break, so we rely heavily on types and testing to
         | know if a change is safe.
         | 
         | I think it's a shared responsibility, the person who wrote the
         | old code needs to have written it in a way that makes it hard
         | to inadvertently break, through testing or otherwise. The
         | person who modifies the code needs to understand what to do if
         | a test fails (modify the test? modify the code?) If we had to
         | guarantee that our changes are never going to break any code
         | downstream, no matter how poorly tested or engineered, we
         | couldn't make changes at all.
        
       | Swiffy0 wrote:
       | Isn't the whole point of the term "invariant" that it describes
       | something as unchanging under specific circumstances.
       | 
       | e.g.
       | 
       | The sum of the angles of triangles is 180 degrees in the context
       | of euclidean geometry. However, if we project a triangle on a
       | sphere, this no longer holds. So the sum of the angles is an
       | invariant under euclidean geometry.
       | 
       | On the other hand, the value of PI is a constant because it stays
       | the same regardless of the circumstances. That's why all the
       | numbers themselves are constant as well - the number 5 is number
       | 5 absolutely always.
       | 
       | So if you have a value that changes over time, it is definitely
       | not a constant. It could be invariant, if you, e.g. specify that
       | the value does not change as long as time does not change. Your
       | value is now an invariant in the context of stopped time, but it
       | can never be a constant if there is any context where it does
       | change.
        
         | pixl97 wrote:
         | >the value of PI is a constant
         | 
         | You mean "about 3" right?
        
         | eschneider wrote:
         | "the number 5 is the number 5" isn't so much invariant as
         | axiomatic.
        
       | larsrc wrote:
       | Usually an invariant is something you explicitly state about a
       | data structure or algorithm, and which must hold before and after
       | an operation, but can be temporarily violated.
       | 
       | What you are talking about here are assumptions, which are
       | usually implicit and sometimes not even part of the thought
       | process. One purpose of writing a design document is considering
       | and stating the relevant assumptions.
        
       | mmis1000 wrote:
       | It's still way easier to handle it when you know where it will
       | break(even it is far way from you) instead of just let it break
       | everywhere. Make it just explode and fix it is still much better
       | than a unknown bug that you never know if it is fixed haunting
       | you forever.
       | 
       | Rust for example. Enforce the invariant everywhere. It's probably
       | harder to build, but it is also easier to find out where and why
       | it won't instead of have infinite memory safety problems
       | everywhere.
       | 
       | And about implicit invariant? Probably a simple code change can
       | fix it, a test would cover it, or an assertion. Or you need a
       | whole new language to describe and test it safely(like rust)
       | because the one you are using just can't. People are not even
       | close to find out the answer of this problem, but the direction
       | is to make it explicit.
        
       | fedeb95 wrote:
       | implicit or explicit assumptions, or invariants that can actually
       | vary, should be minimized. Otherwise, try to explicit them with
       | guards and exceptions. This should never happen, so I'll do that
       | - when you think this, stop and put an "if(this) then Error" in
       | your code. Breaking is better than not breaking, lurking, and
       | costing money later. My opinion.
        
       | agentultra wrote:
       | Specifications can change over time. It sounds to me as though
       | the developer who made the change to the system that invalidated
       | the invariant of the original specification wasn't aware of the
       | spec in the first place. Maybe they should have worked on the
       | specification first before proceeding.
       | 
       | Sounds like a communication issue to me.
        
       | shermantanktop wrote:
       | The way invariants are used can be different than "this will
       | always be true."
       | 
       | It's a problem solving tool. I sometimes use the term to mean "I
       | will assume this to be true in order to solve an intractable
       | problem. If that succeeds, I need to find a way to guarantee this
       | is actually invariant."
        
       ___________________________________________________________________
       (page generated 2024-04-23 23:02 UTC)