I worked for a company at some point where, due to the how the original product was built, testing before a release took between 3 and 5 days and was generally quite brittle.
Within my first three months there, we had business requests to improve that. Invariants, and generally speaking, contracts for our models helped us out. Because we were able to run effective, cheap, fast tests, that proved our implementations were satisfying our requirements, pre and post conditions, invariants, the whole shebang, at the contract level, without even worrying about the actual underlying implementation.
This greatly simplified the release tests, and it meant we were able to release much quicker, and with a higher confidence than before
Class invariants are fundamental to Functional Programming too
They are the core principle our of much-beloved Lexi Lambda's "Parse, Don't Validate" approach.
One example might be a dataclass called "NonEmptyAlphanumericString" in Kotlin:
data class NonEmptyAlphanumericString(val value: String) {
init {
require(value.isNotBlank()) { "String cannot be blank" }
require(value.all { it.isLetterOrDigit() }) { "String must be alphanumeric" }
}
}
Here we have a data type that is parsed from a basic "String" and enforces the invariants that the string is neither empty, nor contains non-alphanumeric characters.
If it fails those invariants, we won't get a valid instance of "NonEmptyAlphanumericString" and "require()" will throw.
(Though is this is a bit different because there is purposefully no mutable state here, if you enforce invariants again it's through creating a new instance of the immutable class)
> Is it really parsing anything, or is it merely validating its input and returning a result?
Ok. Now I guess I’m on board. Just feels like the word is wrong.
> Consider: what is a parser? Really, a parser is just a function that consumes less-structured input and produces more-structured output. By its very nature, a parser is a partial function—some values in the domain do not correspond to any value in the range—so all parsers must have some notion of failure.
Ok. It still feels like there’s a better word. “Constrain” might sit better with me, though I’ve not spent enough time to really worry about its fit.
I didn't get it any of the times I read it either FWIW, it just randomly clicked one day for me while coding and I connected it back to the article
I'm not the smartest tool in the shed, if I could dumb it down a little (sorry Alexis, and hopefully I'm doing you justice here!), I think it boils down to this:
In many programs, you have logic like the below:
function processOrderRequest(items: List<Item>, customer: Customer) {
if (items.isEmpty()) {
throw new IllegalArgumentException("Items cannot be empty")
}
if (customer.isBanned()) {
throw new IllegalArgumentException("Customer is banned")
}
// ...
orderAPI.placeOrder({
// .. final order object with some logic
})
}
This makes sense, but if you look at it another way, what you're trying to encode is some function like this:
function processOrderRequest(
items: ValidatedItemList, // A non-empty list of items we've know are okay to send
customer: VettedCustomer // A customer we've vetted and know is okay to place an order
) -> PlacedOrderRequest {
// ...
}
What this does is allow you to move the domain of validation away from the fragile business logic of your program, and into the deeply-embedded domain of your data types.
It also allows you to control and refactor the validation logic much easier, since they live in your data-types and at boundary points
The key of the idea is you're not asserting that predicates on values are true, you're recognizing whether a value (of a type with few guarantees) conforms to the guarantees of a type (implicitly, a type with more guarantees), and transforming the value into a value of the output type.
Parsing might not be the right word, but it's definitely an example of the above; we transform a string into a tree of tagged nodes.
Rather than "parsing", is it a form of type casting?
To me it feels like downcasting in OOP from a parent class (more general) to a child class (more specific).
In Swift there is the "conditional downcast" operator `as?` which does what the article discusses with the potentially failing conversion to `Maybe thing`: it returns the new more constrained as `.Some(thing)`, or returns `.None` if it is not the appropriate type.
You can also go between non-hierarchical types with casting in most languages, like from ints to floats to strings, etc.
Following from all of that and your language suggestion, I might call the practice a "constraining type cast".
---
Although, all of that written, and I expect it's conversion rather than casting.
> It still feels like there’s a better word. “Constrain” might sit better with me, though I’ve not spent enough time to really worry about its fit.
In clojure’s “spec” library, it’s called “conform”. Confirm will validate and parse the input data based on the supplied spec and will either return a special “invalid” type or a structured value based on the structure the spec defined (which often pulls things out of the input data structure and returns this data as named fields in an output data structure — basically it passes abr structures the input).
It's interesting that in web paradigm dynamic languages and low-level systems languages, defensive programming assertions of invariants are widely dismissed. Validation and sanitizing of external input is often lacking and the source of many security issues.
The problem domain is that most programming languages and application frameworks do a piss-poor job of separating and managing business logic around data storage and data mutation. MVC doesn't go nearly far enough. OTOH, there are fancy enterprisy business rule transaction systems that are divorced from most platforms.
I think the key here is standardizing formal business rules in a portable "EBNF"/"markup" manner to be language and framework agnostic while not being laborious as COBOL-like Cucumber testing.
> Class invariants — consistency constraints preserved by ev-
ery operation on objects of a given type — are fundamental to building,
understanding and verifying object-oriented programs.
I think calling them "consistency constraints" would have made everything much easier to understand.
Although I'll admit that I have a strong bias against nouns that can also be adjectives in programming terminology (invariant, generic, atomic, primitive etc). There's something about them that makes my brain spin an extra cycle to process them.
Java came along with a free compiler and Meyer still insisted on charging for his. That’s what killed Eiffel. And a doomed a generation and a half of devs to shitty object orientes analysis, culminating in Design Patterns, and reaching its absurd conclusion in Enterprise Design Patterns (institutionalized architectural astronautics). Which incidentally was published after Alexander had already moved on to new organizational principles.
But "invariant" has application in various areas, e.g. you can have a loop invariant, or function invariant.
The concept/wording makes more sense on this case than "loop consistency constraint", and then you get the benefit of the concept carrying over to different places.
There was a now-defunct python-like language built on .Net called Cobra that had class invariants and other contract functionality. It generally speaking had a lot of ideas focused on high quality programming built into the language which made it great to program against. I wish it had caught on....
Invariants (of various kinds) are great concepts. There is a good reason you won't find these in mainstream languages though. Invariants are mind-bogglingly difficult to come up with and incrementally update.
For any non-trivial code, you will be spending significantly more time thinking about invariants than implementing functionality.
If you think a borrow-checker is a cognitive overhead, invariants are significantly more so.
I am sure there are people who would consider than a good thing, and even argue that it results in better code in the long run. Just not enough for pushing these into mainstream languages.
Invariants are the single most useful concept, in any programming environment, for making and keeping correct systems. They are necessary to reason mathematically about systems. In interviews, I find them very inconsistently taught.
Invariants are not a substitute for anything else, but bolt on cleanly to whatever else you are doing. Whatever power functional programming offers comes from having numerous invariants baked in.
i like the idea of invariants. i even like their implementation in eiffel. i even quite like eiffel as a language. too bad that Meyer never produced a useable implementation of it, and that he was so destroyed by envy over the success of C++.
In business and administrative applications, there are very few reliable "invariants". Demo examples often use physics and geometry for a reason: Mother nature rarely changes those. Biz is different: new legislation, corporate edicts, new CEO's etc. can "rewrite" anything. These animal and shape demos gave too many the wrong impression of categories and types, creating "spaghetti class" messes that are still being cleaned up today.
Has-A has replaced Is-A as the better modelling mechanism in my domain, but it turns out relational does Has-A better than OOP modelling because it's based around set theory, not category hierarchies.
I don't see any problem with evolving invariants over time as business requirements change. The point is to be clear about your invariants for any given release.
Listing and implementing/asserting invariants of data structures feels underrated.
To me, it makes so much more sense than trying to sell writing tests to my stakeholders.
Various forms of checking invariants is applied in all kinds of "real life, professional programming" ranging from test suites to e.g. invariants checked by your database schema. Most code contains huge amounts of manually written invariants (every check to determine if a value is valid before e.g. setting an instance variable in a class is an attempt to maintain an invariant), and often it'd be a lot faster to get stuff to work with more comprehensive ability to enforce invariants - whether just during development or in production too.
>> Sorry, 99% of these buzzwords aren't applicable in real life, professional programming.
Professionals use proven tools and techniques to get the job done. Design by Contract (DbC) is a proven technique that ensures code is robust and does what it is supposed to do.
>> Business just wants stuff to work, no matter how you make it work.
Business cares if shody code has security vulnerabilities that lets money or customer data be stolen. DbC has been proven to help prevent shody code.
If your application has any reasonably complex data, validating that data when it is being added into the system (i.e. if it meets the invariants) prevents bugs later down the line. If the data is different to what the application expects, you need to spend time identifying that data, and correcting it. If the data is complex (e.g. book metadata) it can be expensive to correct.
Having up-front validation of the input (e.g. checking book subject values) prevents a whole class of errors (such as spelling errors between country names), and enforces the contract between the data and application -- the list of things validated should be based on how the application works, such as required fields/values. If there is a new value that the business need, you then know you need to make an application change in order to support that value.
Some values (like the list of countries) would be an open list, and would be best stored in the database/application as a list that can be updated outside of the code changes. Some other values (esp. those that need different code/logic to function) are closed lists that should be maintained and validated in the code (e.g. as enumeration values).
Yes but also no. This kind of things, usually called "checks" in the wild, are nearly everywhere in code that works (i.e. wasn't fixed in a while and is triggered often)
In my experience, class invariants are among the very most useful techniques for enabling rapid development of features while managing mutable data. They allow you to "make stuff work" at a faster pace than otherwise.
Class invariants are very useful in domain-driven development allowing you to have contracts between modules/services that can’t break the underlying business rules. You would probably like them if you spent some time reading about them and compare it to your error/exception handling logic to deal with incorrect values.
Within my first three months there, we had business requests to improve that. Invariants, and generally speaking, contracts for our models helped us out. Because we were able to run effective, cheap, fast tests, that proved our implementations were satisfying our requirements, pre and post conditions, invariants, the whole shebang, at the contract level, without even worrying about the actual underlying implementation.
This greatly simplified the release tests, and it meant we were able to release much quicker, and with a higher confidence than before