I’m not really suggesting there needs to be an alternative. Using newtypes to achieve safety via encapsulation is fine, good even, and I say as such at several points in the article.[1] The point is twofold:
1. A lot of uses of newtypes in the wild are “safety theater” and provide zero actual safety benefit.
2. The uses of newtypes that do add safety provide it in a weaker sense than correct-by-construction data modeling.
The point is not that newtypes are always useless. In fact they are often by far the most practical tool for the job. The blog post just advocates being conscious and considerate of the limitations of the techniques.
Having a Magnitude newtype over Int doesn't change the range of permissible values but does prevent using a Length where a Magnitude was expected, for example.
In all typed languages you'll eventually see a function with multiple arguments of the same type and have to check the documentation (or worse, implementation) to know what each is for. Having a zero cost type alias to make it clear is an improvement and IMO is a form of type safety.
Having a data type that can hold illegal values is a bad idea. Using an Int for Length implies you know what to do with negative lengths, for example. A newtype won't help there.
It's real-world data, which is rarely 100% clean and correct. If you can only represent clean and correct data, you probably can't handle all the data you'll need to.
For an email, a "newtype Email = Text" is, IMO, correct. You can break it down as far as you need though: "data Email = Email (Maybe Name) LocalPart Domain" eg, with the three sub-types being newtypes of text. If you try to go further you'll run into problems that the best definition of "valid" is "works when used."
The situation for URLs is similar.
ZIP codes is an even trickier one - if you can only represent genuine ZIP codes, what happens if you need to represent an address where someone's accidentally transposed two digits and wound up with an unused ZIP code? If you're not US domestic only, what do you do for the four billion or so humans who don't have a structured address at all?
(There's a reason vCard gave up on forcing structured addresses and added a "just whatever this text chunk here says" alternative).
If you want to represent "validated version" data then IMO it's sufficient to use a "data ValidatedEmail = ValidatedEmail Email" with a non-exported constructor and a "validateEmail :: Email -> Maybe ValidatedEmail" function.
If you're, say, the USPS and really must represent valid ZIP codes and only valid ZIP codes, you'll perhaps wind up in the rabbit hole of defining a hierarchical hot mess of sum types. Or pragmatically accept you will risk invalid ZIP codes to avoid needing to do that.
How would you deal with text that can only contain certain characters for example?
There are type systems that let you declare a new type to be a sub-range of an existing type, but Haskell's is not one of them.
https://en.wikibooks.org/wiki/Ada_Programming/Types/range
As you can see from that link, it's bounds checked at runtime. You could have a modulo type that wraps on overflow, which is of course what most numeric types are in most programming languages, including Haskell, but the modulus is fixed at some power of two for obvious reasons.
There's only one thing I truly value out of a good type (or static analysis) system, and that's enabling fearless refactoring. I want the type system to pick up on any downstream consequences of a change I make rather than finding out by phone call at 3am the day after a deploy that some edge case got overlooked.
Dependent types can (in theory) give that over smart constructors. If my parser should leave me with, say, a sorted vector, or an assertion that if one field is some value then some other field is Just something, then I'd like to be able to rely on those facts in downstream code making use of the results of the parser. If it's just a smart constructor as we usually write them a later refactor (sorting on a different key, for example) doesn't change anything that any other bit of code can be verified against.
A dependent type carrying a proof that a vector is sorted based on some criteria and an assertion about the existed of some value can be verified at compile time.
I'm not sure what the status is of dependent types for Haskell. You could do similar propositional assertions now with Ghosts of Departed Proofs, and LiquidHaskell exists for these sorts of assertions, so dependent types aren't the only path to this kind of static safety, either. I won't claim that this is practical - though I would be surprised if it's not practical to do in small doses.
Nevertheless, I would still just be reaching for smart constructors in current day Haskell.
parseLength :: JSON -> Either Error (Sigma (l : Length). lengthToDouble l >= 0.0)
is reasonable, and I'm sure somewhere in the literature there's an indexed monad or something for building parsers/validators out of these, that keeps track of the properties while combining parsers like these.You have a constructor function which does the input check, and it lives in a module that only exports the smart constructor, not the type constructor. It doesn’t matter if another module deconstructs the type, since the input was already checked.
For example:
newtype Username = Username Text
mkUsername :: Text -> Either Text Username
mkUsername t = if t == “” then Left “Username cannot be blank” else Right (Username t)
If you don’t export the constructor for Username, and only export mkUsername, it doesn’t matter if someone deconstructs the Text from Username, because it must already be validated.
It’s like saying “if you forget to sanitize input it won’t get sanitized.” Okay?
The only way to acquire a ValidatedFoo is to pass an UnvalidatedFoo into ValidateFoo(). The outer layer of the code deals in UnvalidatedFoos, but past that point you must have a ValidatedFoo to continue.
I think the main point of this article is that newtypes aren't an application of "parse, don't validate". If you validate that user input doesn't contain XSS, then wrap it in a `SafeHtml` newtype, that's validation, not parsing. The article outlines ways that you could wrap the user input but forget to validate.
By contrast, suppose you have a type like `type SafeHtml = TextNode Text | DivNode SafeHtml | ...`. You parse the user input into a value of this type. If the parse fails, it's because the user input contained something unsafe. The article contends that it's harder to accidentally wrap an unsafe string, since you have to parse a safe HTML string to get a value of type `SafeHtml`.
(Although I'm not sure I buy that last point. With both methods you'd want to keep the `SafeHtml` type's constructors in a module and not export them. Otherwise you could wrap arbitrary text in the newtype or in the `TextNode` constructor. Both seem equally possible to me.)
I don't see how that helps, though, because there could be bugs in your language extension, and it has to be maintained. It's just going about it the hard way. It would be sort of like avoiding "unsafe" in Rust by extending the compiler instead.
This is essentially how React.js works; you don't need to tag strings as safe or unsafe because strings are never treated as HTML, only constructed HTML values (via JSX) are treated as HTML.
It's like SQL parameterized queries: delay the concatenation until the last possible moment to reduce the area where errors can occur.
But I think maybe that just moves the opportunity for error to the render function, so I don't know if that is really an improvement on "smart constructors".
What's a better alternative though?
I like this approach for tagging e.g. user input that has been checked to be free of HTML/JS injection hacks to stop you from outputting user input that hasn't been checked first (Angular does something like this with TypeScript I think). Obviously you could tag something as safe that isn't by mistake but at least the places in your code this decision is made is explicit and you can't forget to do it.
I'm not seeing a practical alternative that would be significantly better. If you want to be able to capture any property you can think of in the type system (like bounded integers or bounded lists), you're inevitably going to need dependent types and they're about as far from practical as you can get (where either you or the computer will then have to write difficult maths proofs before you know your code is correct).