4 ms·
> My point was that focusing on an empty string as the hill to die on was a touch artificial. There must be a term for this phenomenon. My original reply that
by ragnese 5y ago
> My point was that focusing on an empty string as the hill to die on was a touch artificial.
There must be a term for this phenomenon. My original reply that sparked this thread could be tl;dr as "I get frustrated with most programming languages. People at work know me as the guy that always complains about programming languages. One example of that is that I recently complained about most languages making it hard to statically guarantee that an input string is non-empty."
But then I get painted as "dying on a hill" for non-empty strings. It was one example, and it's not even the most important complaint I have about current programming languages. It was just one that came up this week at work because we literally had to deal with a bug from a REST API of a big company because THEY sent an empty string in a JSON payload that isn't supposed to be empty. Their code obviously missed a check somewhere and instead of sending a 404 response, they sent a bad payload (according to their own docs).
> My argument for why not, is that I don't think it pays off for most uses. You will wind up putting a ton of logic into the types, but then you have to do a ton of logic to correctly serialize into the types you have.
>
> Do I think there are times/places this could pay off? I'd hope/expect so. But where the data hits the wire is likely not where you can set many of these constraints such that your type system can really help with them.
This is hard to debate because we're speaking in very abstract terms. Obviously different domains will have different needs, etc.
So, unfortunately, I'm not sure I follow your argument for "why not". But here's my argument for "why". A lot of times, when we design software, it ends up working in "layers". Some function calls some other function calls some other function, etc.
If that "bottom" function requires something like a non-empty string (maybe that function is going to print a mailing label and it would be ridiculous to waste printer time on a blank label), you have two options: compile time enforcement or runtime enforcement.
In my experience, the majority of the time, "we" choose runtime enforcement, even in statically typed languages. What does runtime enforcement look like? Usually it's one of two things: you throw an exception or you return some kind of failure value.
If you throw an exception, it bubbles all the way up and your top level main loop has to catch it and understand how to handle it. That somewhat implies that your top level has to know everything that could go wrong at any layer of your code.
In addition, you now have a dilemma. You know that if you pass an empty-string through, that you'll eventually hit the function that requires a non-empty string. So you have two sub-options for this option. You can validate at the top level and pass it only if it's valid, or you can pass everything through and potentially do a lot of computational work before hitting the failing function and throwing away all that work. Most people choose to validate at the top level. So, you're validating at the top level anyway, and you're possibly validating in TWO places (the function doesn't know who might call it)- not DRY.
Other issues with the (unchecked) exception approach: It ALSO means that the type signature on your bottom-layer function is LYING. It "said" that you could pass it a string and it would return Whatever. It was wrong. You passed a string and instead of returning a Whatever, it started unwinding the stack for you. That's not static typing. Callers of this function can't trust its type signature to be complete. Instead they now have to read documentation (hopefully you wrote some). But if they already have to read documentation to understand what your function accepts and returns, why did we bother write the types at all? Just use Python or JavaScript and don't put types on anything. You just have to read the docs to know how to call it and the types will never get in the way.
The other runtime option is that your function might return a value that indicates failure. Some languages have "Result" or "Try" types. But if you do that, then the function that calls that bottom function has to handle the return value. There's a good chance that the "second layer" function can't really handle the failure, so it ALSO has to return a failure value. Etc, etc, until every function in the call chain has altered its return type to indicate that it may fail. Then your top level loop inspects the return value and handles the failure. I include Java-style checked exceptions in this category and not in the above "exception" category.
This return-failure-value approach pollutes all layers of your code even worse than if you just had a NonEmptyString type! Instead of the top level inspecting the bubbled up failure value after doing a bunch of computation, it could have just tried creating a NonEmptyString type from the input. If it failed, then the top-level handles the failure same as before, but didn't waste a bunch of CPU and clock time doing computations before hitting the failure. Furthermore, it's very DRY because the validation logic is in the type itself, either in some kind of type refinement mechanism, or in a factory function, etc.
Furthermore, the NonEmptyString type approach gives you more compile-time safety from bugs. If you try to call that bottom function with a maybe-empty-string, it won't compile. In the other cases, you'll only find out at runtime, even though you KNOW ahead of time that it's illegal to do so. I hope your tests cover everything.
- taeric 5y agoFirst, an ack that I am almost certainly not touching all of your points. My apologies on that. I think I am going to lean in on our arguments. I would rather change mine from "why not" to "why this isn't worth forcing". That is, on the logic and aims, I fully agree with you. It is more that in practice, I have seen this fail too many times. I expect and look forward to it succeeding some day, but I still caution against jumping all in. I should also ack that I am pretty happy with how common lisp does this. By mixing in read time and evaluation time, you can actually get a lot of this. I'm on my phone right now, but (format nil "~{") will not evaluate in sbcl. Instead, it will indicate an error in the format string. This is similar to how c will fail the build with Wall and Werror on similar bad format strings. Difference is that in lisp, you can add such evaluation time checks as a user. As for why we do so much at runtime, my assertion is we check at runtime that which is determined at runtime. When getting data from outside the static program, there is little help the static type system can offer. So, deep in your system, get things out of strings and primitives as soon as you can. Don't pass a string username, pass a username. This gets you essentially what you want, but flags where the invalid value could have come from, as well as where it could have been used. And lets you add on other validations. AuthenticatedUsername and UnauthenticatedUsername, for example. I get that you called those out as another category of error, but types are explicitly for that, as well. And finally, in somewhat reverse form, I agree that arguments on specifics as a proxy for general are frustrating. For my part, apologies on adding to it.
- ragnese 5y ago> As for why we do so much at runtime, my assertion is we check at runtime that which is determined at runtime. When getting data from outside the static program, there is little help the static type system can offer. So, deep in your system, get things out of strings and primitives as soon as you can. Don't pass a string username, pass a username. This gets you essentially what you want, but flags where the invalid value could have come from, as well as where it could have been used. And lets you add on other validations. AuthenticatedUsername and UnauthenticatedUsername, for example. See, but that's exactly what I'm saying. Get out of primitives ASAP. I just want our languages to allow us to do that with minimal ceremony and minimal performance cost. Most languages fail on at least one of those. As a result, most developers don't do it, and I'm stuck interacting with their code.