The concept of class invariant in object-oriented programming
arxiv.org
arxiv.org
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
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.
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
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.
In which case, we can check that a variable can be type-asserted to a given subtype.
A (sub)type being a set of propositions (i.e. defines a set of values).
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
Once I grokked this I was like, whoa. Neato idea.
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.
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.
struct NonEmptyAlphanumericString {
let value: String
init(string: String) throws {
guard !string.isEmpty else {
throw Errors.emptyString
}
guard string.isAlphanumeric else {
throw Errors.nonAlphanumericString
}
value = string
}
enum Errors: Error {
case nonAlphanumericString
case emptyString
}
}Though yours also has the benefit of typed errors
I've always liked Swift, but never written it. It seems like Kotlin and Swift are the most two similar languages.
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.
Anyways,when i describe invariant i also say something is in itself consistent or true.
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.
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.
Example: "please don't delete item X from the dictionary out of a traversal callback visiting item X." :)
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.
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.
Business just wants stuff to work, no matter how you make it work.
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.
For sure. But from there, it's our professional responsibility to figure out good ways to make it work for as long as the business need is there.
Imagine a doctor saying: "sorry, 99% of these buzzwords like 'endocrine system' aren't applicable; patients just want to feel good."
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).