7 ms·
> Newtypes are useful when carefully applied, but their safety is not intrinsic, no more than the safety of a traffic cone is somehow contained within the plast
by seanwilson 6y ago
> Newtypes are useful when carefully applied, but their safety is not intrinsic, no more than the safety of a traffic cone is somehow contained within the plastic it’s made of. What matters is being placed in the right context—without that, newtypes are just a labeling scheme, a way of giving something a name.
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).
- tbroadley 6y agoApplying the author's "parse, don't validate" concept, I think you'd parse the user input into a "safe HTML" type with a definition that excluded the language features that enabled those attacks. It might not be worth writing such code for a single project, but I could get behind a library that did that.
- p1necone 6y agoThis is an approach I've used in C#/Java - you have 'UnvalidatedFoo' and 'ValidatedFoo' types, UnvalidatedFoo has a public constructor, ValidatedFoo has a private constructor. 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.
- dodobirdlord 6y agoAn extremely good example I've found for this is canonicalization in all forms. Frequently there are scenarios where there are many ways to represent something, but only one official Right Way. https://en.wikipedia.org/wiki/Canonicalization#Usage_cases https://en.wikipedia.org/wiki/Canonicalization#Usage_cases
- nyanpasu64 6y agoDoes "parse, don't validate" apply regardless if the resulting type is a newtype or constrained by construction?
- tbroadley 6y agoI'm not sure I see how "parse, don't validate" would apply when using a newtype—would you mind explaining further? 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.)
- nyanpasu64 6y agoReturning an Option<newtype> is closer to "parsing" than returning a bool.
- alexmingoia 6y agoThe way to ensure type safety of new types is smart constructors: https://wiki.haskell.org/Smart_constructors https://wiki.haskell.org/Smart_constructors 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.
- shawnz 6y agoThe article gives some examples why smart constructors don't fully solve the problem in the section "Newtypes as tokens"
- alexmingoia 6y agoYeah, I don’t find those points very convincing. Obviously if you export other ways to construct the type, it doesn’t work... It’s like saying “if you forget to sanitize input it won’t get sanitized.” Okay?
- imoverclocked 6y agoThe entire point of a good type system is to reduce the need for extra cognitive diligence around your data/code. The more corner cases you have to be careful around, the less helpful the type system is.
- deleted 6y ago[deleted]
- remexre 6y agoI've gotten bit by the Generic one in real code before... I'm sure if I used Haskell for RESTful web stuff more, I'd eventually get bit by something similar for Aeson too.
- shawnz 6y agoMaybe you can solve it like this: internally represent the document as DOM objects. Have element nodes contain a list of children which can be either raw strings (representing text nodes), or other element node objects. The render function of element nodes would know to escape the contents of text nodes before concatenating them with their rendered element node siblings. It would be type safe since it knows all the types of children it can contain. 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".
- codebje 6y agoIn Haskell, you would use an algebraic data type that can only hold legal values. You would typically only use a newtype when you're just giving a new name to the same type. 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.
- alexmingoia 6y agoAre you suggesting using Proxy to create a type that masks an Int? How would you deal with text that can only contain certain characters for example?
- codebje 6y agoIn the Length case I'm more suggesting to use an unsigned integer type than any more complex type system trickery to specifically limit the range. 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.
- alexmingoia 6y agoDependent types don't provide any safety over smart constructors for parsing, which is the most common use case for types with restricted values. Validating user input, parsing emails or URLs, etc. all must deal with an invalid case at runtime, and dependent types are of no use there.
- remexre 6y agoWhat dependent types would protect you against would be the parser itself returning data. I think it'd be quite difficult to do this for the case of an email or URL, but for cases like "length values must be non-negative," a parser/validator with a type like 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.
- skybrian 6y agoIt seems to me that there is no practical alternative. The impractical alternative is to extend the language so that its type system implements the feature you want. (For example, adding bounded integers.) 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.
- p1necone 6y agoThere's a lot to be gained by isolating the place where the validation needs to be done, and the source of the bugs could be to one place though. Would you rather have one bit of code that checks 1<=X<=5, or 100 call sites all doing the same thing slightly differently?
- skybrian 6y agoThere ways to do that without type system level hacking.
- lexi-lambda 6y ago> What's a better alternative though? 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. [1]: https://news.ycombinator.com/item?id=24964494 https://news.ycombinator.com/item?id=24964494
- PhineasRex 6y agoSo in the example where you're constructing HTML, you would never use strings to represent your HTML. So your template might look something like ``` P(Text(someUserText), I(someItalicized)) ``` 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.