Wait what? I don't get why performance improvement implies reliability and incident improvement.
For example, doing dangerous thing might be faster (no bound checks, weaker consistency guarantee, etc), but it clearly tend to be a reliability regression.
First, if a performance optimization is a reliability regression, it was done wrong. A bounds check is removed because something somewhere else is supposed to already guaratee it won't be violated, not just in a vacuum. If the guarantee stands, removing the extra check makes your program faster and there is no reliability regression whatsoever.
And how does performance improve reliability? Well, a more performant service is harder to overwhelm with a flood of requests.
It does not need to be an explicit check (i.e. a condition checking that your index is not out of bounds). You may structure your code in such a way that it becomes a mathematical impossibility to exceed the bounds. For a dumb trivial example, you have an array of 500 bytes and are accessing it with an 8-bit unsigned index - there's no explicit bounds check, but you can never exceed its bounds, because the index may only be 0-255.
Of course this is a very artificial and almost nonsensical example, but that is how you optimize bounds checks away - you just make it impossible for the bounds to be exceeded through means other than explicitly checking.
Based on what I observe as an occasional tutor, it looks like compiler warnings & errors are scary for newcomers. Maybe it's because it shares the same thing that made math unpopular for most people: a cold, non-negotiable set of logical rules. Which in turn some people treat warnings & errors like a "you just made a dumb mistake, you're stupid" sign rather than a helpful guide.
Weirdly enough, runtime errors don't seem to trigger the same response in newcomers.
Interesting angle: Compiler errors brings back math teacher trauma. I noticed Rust tries to be a bit more helpful, explaining the error and even trying to suggest improvements. Perhaps "empathic errors" is the next milestone each language needs to incorporate.
I suddenly understand part of why experienced programmers seem to find Rust so much more difficult than those who are just beginning to learn. Years of C++ trauma taught them to ignore the content of the error messages. It doesn't matter how well they're written if the programmer refuses to read.
Interesting. I think over the long term many people come to realise it's better to know at compile time (when they mistype something and end up with a program that runs but is incorrect it's worse than not running and just telling you your mistake). But perhaps for beginners it can be too intimidating having the compiler shout at you all the time!
Perhaps nicer messages explaining what to do to fix things would help?
That's surprising because runtime debugging depends on the state of the call stack, all the variables, etc. Syntax errors happen independent of any of that state.
There's no special keyword, just a "generic" type `IO<T>` defined in standard library which has a similar "tainting" property like `async` function coloring.
Any side effect has to be performed inside `IO<T>` type, which means impure functions need to be marked as `IO<T>` return. And any function that tries to "execute" `IO<T>` side effect has to mark itself as returning `IO<T>` as well.
> Maybe in the future we don't use these things to write programs but if you think we're going to go the rest of history with just natural languages and leave all the precision to the AI, revisit why programming languages exist in the first place.
> The LLM having the ability to write code doesn't change that we have to understand it; we just have one more entity that has to be considered in the context of writing code. e.g. sometimes the only way to get the LLM to write certain code is to feed it other code, no amount of natural language prompting will get there.
You don't exactly need to use PLs to clarify an ambiguous requirement, you can just use a restricted unambiguous subset of natural language, like what you should do when discussing or elaborating something with your coworker.
Indeed, like terms & conditions pages, which people always skip because they're written in a "legal language", using a restricted unambiguous subset of natural language to describe something is always much more verbose and unwieldy compared to "incomprehensible" mathematical notation & PLs, but it's not impossible to do so.
With that said, the previous paragraph will work if you're delegating to a competent coworker. It should work on "AGI" too if it exists. However, I don't think it will work reliably in present-day LLMs.
> You don't exactly need to use PLs to clarify an ambiguous requirement
I agree, I guess what I'm trying to say is that the only reason we've called constructed languages "programming languages" for so long is because they've primarily been used to write programs. But I don't think that means we'll be turning to unambiguous natural languages because what we've found from a UX standpoint it's actually better for constructed languages to be less like natural languages, than to be covert natural languages because it sets expectations appropriately.
> you can just use a restricted unambiguous subset of natural language, like what you should do when discussing or elaborating something with your coworker.
We’ve tried that and it sucks. COBOL and descendants also never gained traction for the same reasons. In fact proximity to a natural language is not important to making a constructed languages good at what they're for. As you note, often the things you want to say in a constructed language are too awkward or verbose to say in natural language-ish languages.
> terms & conditions pages, which people always skip because they're written in a "legal language"
Legalese is not unambiguous though, otherwise we wouldn’t need courts -- cases could be decided with compilers.
> using a restricted unambiguous subset of natural language to describe something is always much more verbose and unwieldy compared to "incomprehensible" mathematical notation & PLs, but it's not impossible to do so.
When there is a cost per token then it become very important to say everything you need to in as few tokens as possible -- just because it's possible doesn't mean it's economical. This points at a mixture of natural language interspersed code and math and diagrams, so people will still need to read and write these things.
Moreover, we know that there's little you can do to prevent writing bugs entirely, so the more you have to say, the more changes you have to say wrong things (i.e. all else equal, higher LOC means more bugs).
Maybe the LLM can write a lower rate of bugs compared to human but it's not writing bug-free code, and the volume of code it writes is astronomical so the absolute number of bugs written is probably also enormous as well. Natural language has very low information density, that means more to say the same, more cost to store and transmit, more surface area to bug check and rot. We should prefer to write denser code in the future for these reasons. I don't think that means we'll be reading/writing 0 code.
> Code is much much harder to check for errors than an email.
Disagree.
Even though performing checks on dynamic PLs is much harder than on static ones, PLs are designed to be non-ambiguous. There should be exactly 1 interpretation for any syntactically valid expression. Your example will unambiguously resolve to an error in a standard-conforming Python interpreter.
On the other hand, natural languages are not restricted by ambiguity. That's why something like Poe's law exists. There's simply no way to resolve the ambiguity by just staring at the words themselves, you need additional information to know the author's intent.
In other words, an "English interpreter" cannot exist. Remove the ambiguities, you get "interpreter" and you'll end up with non-ambiguous, Python-COBOL-like languages.
With that said, I agree with your point that blindly accepting 20kloc is certainly not a good idea.
Tell me you've never written any python without telling me you've never written any python...
Those are both syntactically valid lines of code. (it's actually one of python's many warts). They are not ambiguous in any way. one is a number, the other is a tuple. They return something of a completely different type.
My example will unambiguously NOT give an error because they are standard conforming. Which you would have noticed had you actually took 5 seconds to try typing them in the repl.
> Those are both syntactically valid lines of code. (it's actually one of python's many warts). They are not ambiguous in any way. one is a number, the other is a tuple. They return something of a completely different type.
You just demonstrated how hard it is to "check" an email or text message by missing the point of my reply.
> "Now imagine trying to spot that one missing comma among the 20kloc of code"
I assume your previous comment tries to bring up Python's dynamic typing & late binding nature and use it as an example of how it can be problematic when someone tries to blindly merge 20kloc LLM-generated Python code.
My reply, "Your example will unambiguously resolve to an error in a standard-conforming Python interpreter." tried to respond to the possibility of such an issue. Even though it's probably not the program behavior you want, Python, being a programming language, will be 100% guaranteed to interpret it unambiguously.
I admit, I should have phrased it a bit more unambiguously than leaving it like that.
Even if it's hard, you can try running a type checker to statically catch such problems. Even if it's not possible in cases of heavy usage of Python's dynamic typing feature, you can just run it and check the behavior at runtime. It might be hard to check, but not impossible.
On the other hand, it's impossible to perform a perfectly consistent "check" on this reply or an email written in a natural language, the person reading it might interpret the message in a completely different way.
> Or is a big part of this concept only relevant for strong functional languages with sum types and pattern matching?
It need not strictly be a pure functional language for type-driven style to be usable. Type-driven style only requires the fact that some type cannot be assigned to another type, so it's kind of possible to do even in a language like C, as `int a = (struct Foo) {};` would get rejected by C compilers.
However, I don't think it's doable in languages with structural type systems like Typescript or Go's interface without a massive ergonomic hit for minimal gain. Languages with a structural type system are deliberately designed to remove the intentionality of "type T cannot be assigned to type S" in exchange for developer ergonomics.
> However, is there a similar article but written with more common languages (C#, C++, Java, Go) in mind?
For C#, there's F#-focused article, which I believe some of it can be applied to C# as well:
For modern Java, there is some attempt at popularizing "Data-Oriented Programming" which just rebranded "Type-driven design". Surprisingly, with JDK 21+, type-driven style is somewhat viable there, as there is algebraic data type via `record` + `sealed` and exhaustive pattern match & destructuring.
For Rust, due to the new mechanics introduced by its affine type system, there is much more flexibility in what you could express in Rust types compared to more common languages.
> However, I don't think it's doable in languages with structural type systems like Typescript or Go's interface without a massive ergonomic hit for minimal gain.
Go only has structural interfaces, concrete types are nominative, and this sort of patterns tends to be more on the concrete side.
Typescript is a lot more broadly structural, but even there a class with a private member is not substitutable with a different class with the same private member.
Coming from a more "average imperative" background like C and Java, outside of compiler or serde context, I don't think "parse" is a frequently used term there. The idea of "checking values to see whether they fulfill our expectations or not" is often called "validating" there.
So I believe the "Parse, Don't Validate" catchphrase means nothing, if not confusing, to most developers. "Does it mean this 'parse' operation doesn't 'validate' their input? How do you even perform 'validation' then?" is one of several questions that popped up in my head the first time I read the catchphrase prior to Haskell exposure.
Something like "Utilize your type system" probably makes much more sense for them. Then just show the difference between `ValidatedType validate(RawType)` vs `void RawType::validate() throws ParseError`.
The crucial design choice is that you can't get a Doodad by just saying oh, I'm sure this is a Doodad, I will validate later. You have to parse the thing you've got to get a Doodad if that's what you meant, and the parsing can fail because maybe it isn't one.
let almost_pi: Rational = "22/7".parse().unwrap();
Here the example is my realistic::Rational. The actual Pi isn't a Rational number so we can't represent it, but 22 divided by 7 is a pretty good approximation considering.
I agree that many languages don't provide a nice API for this, but what I don't see (and maybe you have examples) is languages which do provide a nice API but call it validate. To me that naming would make no sense, but if you've got examples I'll look at them.
The point is parse and validate are interchangeable words for the most part. If you’re parsing something you expect to be an int, but it’s a float or the letter “a” is that not invalid? Is this assessment a form of validating expectations? The line between parsing and validating doesn’t exist.
That's true, but then again, don't forget the fact that words might get interpreted as different things by different people. Words like "arrow", "functor", or "validate" might get interpreted slightly differently even between people with the same background.
After all, the meaning of words is just a socially accepted meaning attached to a certain arrangement of symbols. The meaning can be whatever they want it to be. And even though each individual might interpret it slightly differently, as long as the interpretation is "compatible", communication between individuals is possible.
Arguably, it's more useful to distinguish between "parse" & "validate" and I agree with that. But based on my own experience and what I've observed when I'm trying to spread type-driven style, it looks like there's no difference in meaning between the words "parse" and "validate" for most developers. Trying to sell type-driven style via the "catchy catchphrase" "Parse, Don't Validate" will certainly backfire, confusing most people rather than making them appreciate the value of it.
In my opinion, it's not worth it to combat this "parse" & "validate" misconception for the sake of the catchphrase "Parse, Don't Validate". Why? Pure FP and type-driven style already put off most people because of the tendency to go with mathematical jargon. Why add even more unnecessary barriers when the core of it is just "utilize your type system"?
I agree with the point of the "Parse, Don't Validate" article, but I strongly dislike the "catchphrase" marketing part.
The fact that we are in disagreement here proves my point. If you pose this question to 10,000 developers, you will get mixed answers. This ambiguity is why I think the phrasing of this article (not the intent) is incorrect.
In the spirit of "Parse, Don't Validate", rather than encode "validation" information as a boolean to be checked at runtime, you can define `Email { raw: String }` and hide the constructor behind a "factory function" that accepts any string but returns `Option<Email>` or `Result<Email,ParseError>`.
If you need a stronger guarantee than just a "string that passes simple email regex", create another "newtype" that parses the `Email` type further into `ValidatedEmail { raw: String, validationTime: DateTime }`.
While it does add some "boilerplate-y" code no matter what kind of syntactical sugar is available in the language of your choice, this approach utilizes the type system to enforce the "pass only non-malformed & working email" rule when `ValidatedEmail` type pops up without constantly remembering to check `email.isValidated`.
This approach's benefit varies depending on programming languages and what you are trying to do. Some languages offer 0-runtime cost, like Haskell's `newtype` or Rust's `repr(transparent)`, others carry non-negligible runtime overhead. Even then, it depends on whether the overhead is acceptable or not in exchange for "correctness".
I would still usually prefer email as just a string and validation as a separate property, and they both belong to some other object. Unless you really only want to know if XYZ email exists, it's usually something more like "has it been validated that ABC user can receive email at XYZ address".
Is the user account validated? Send an email to their email string. Is it not validated? Then why are we even at a point in the code where we're considering emailing the user, except to validate the email.
You can use similar logic to what you described, but instead with something like User and ValidatedUser. I just don't think there's much benefit to doing it with specifically the email field and turning email into an object. Because in those examples you can have a User whose email property is a ParseError and you still end up having to check "is the email property result for this user type Email or type ParseError?" and it's very similar to just checking a validation bool except it's hiding what's actually going on.
> I would still usually prefer email as just a string and validation as a separate property, and they both belong to some other object. Unless you really only want to know if XYZ email exists, it's usually something more like "has it been validated that ABC user can receive email at XYZ address".
> Is the user account validated? Send an email to their email string. Is it not validated? Then why are we even at a point in the code where we're considering emailing the user, except to validate the email.
You are looking at this single type in isolation. The benefit of an email type over using a string to hold the email is not validating the actual string as an email address, it's forcing the compiler to issue an error if you ever pass a string to a function expecting an email.
Consider function `foo`, which takes an email and a username parameter.
> Because in those examples you can have a User whose email property is a ParseError and you still end up having to check "is the email property result for this user type Email or type ParseError?"
In languages with a strong type system, `User` should hold `email: Option<ValidatedEmail>`. This will reject erroneous attempts `user.email = Email::parse(raw_string);` at compile time, as `Result<Email,ParseError>` is not compatible / assignable to `Option<ValidatedEmail>`.
It's kind of a "oh I forgot to check `email.isValidated`" reminder, except now being presented as an incompatible type assignment and at compile-time. Borrowing Rust's syntax, the type error can be solved with
Which more or less gets translated as "Check email well-formedness of this raw string. If it's well-formed, try to send a test email. In case of any failure during parsing or test email, leave the `user.email` field to be empty (represented with `Option::None`)".
> and it's very similar to just checking a validation bool except it's hiding what's actually going on.
Arguably, it's the other way around. Looking back at `email: Option<ValidatedEmail>`, it's visible at compile-time `User` demands "checking validation bool", violate this and you will get a compile-time error.
On the other hand, the usual approach of assigning raw string directly doesn't say anything at all about its contract, hiding the contract of `user.email` must be a well-formed, contactable email. Not only it's possible to assign arbitrary malformed "email" string, remembering to check `email.isValidated` is also programmer due diligence, forget once and now there's a bug.
For example, doing dangerous thing might be faster (no bound checks, weaker consistency guarantee, etc), but it clearly tend to be a reliability regression.
reply