4 ms·
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.
by gavinray 4y ago
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)
- dfee 4y agoI never got it, but on a reread: > 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-validate/ https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
- Snarwin 4y agoMaybe "decode"? As in "decode a Unicode string", or "decode a JPEG image", or "decode an MP3 audio file."
- lalaithion 4y agoThe 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.
- aatd86 4y agoYes, I think another simpler way to do it is to have a set theoretic interpretation of types with subtyping. 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).
- gavinray 4y agoI 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 Once I grokked this I was like, whoa. Neato idea.
- ohwellhere 4y agoRather 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.
- dkersten 4y ago> 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).
- tinglymintyfrsh 4y agoAlso known as validations and business logic. 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.
- clumsysmurf 4y agoThanks for this tip. I often see developers using data classes, with val properties, but not enforcing the invariants are correct with .copy().
- andrekandre 4y agowould that be similar to a throwing init in swift? 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 } }
- gavinray 4y agoSeems identical to me =) 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.
- andrekandre 4y agoyea i think its possible to express ideas from one to another in pretty straightforwards ways for sure