> 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.