Earlier quoted context omitted.
There's a name for this anti-pattern: "Stringly typed"
I've also seen it called primitive obsession, which is also applicable to other primitive types like using an integer in situations where an enum would be better.
How I write HTTP services in Go after 13 years
141–150 of 259 posts
Re: How I write HTTP services in Go after 13 years
#142Earlier quoted context omitted.
The problem is that pattern "fails open." If anyone on the team forgets to define an untrusted string as UnvalidatedString, the data skips validation. If you default to treating primitive types as untrusted, it's hard for someone to accidentally convert an untrusted type to a trusted type without using the correct parse method.
The dual problem would be any function which forgets to accept a ParsedString instead of a string can skip parsing. Both cases appear to depend on there being a "checkpoint" all data must go through to cross over to the rest of the system, either at parsing or at UnvalidatedString construction.
>Both cases appear to depend on there being a "checkpoint" all data must go through to cross over to the rest of the system, either at parsing or at UnvalidatedString construction.
The difference is that if string is the trusted type, then it's easy to miss a spot and use the trusted string type for an untrusted value. The mistake will be subtle because the rest of your app uses a string type as well.
The converse is not true. If string is an untrusted type and ParsedString is a trusted type, if you miss a spot and forget to convert an untrusted string into a ParsedString, that function can't interact with any other part of your codebase that expects a ParsedString. The error would be much more visible and the damage more contained.
I think UnvalidatedString -> string also kind of misses the point of the type system in general. To parse a string into some other type, you're asserting something about the value it stores. It's not just a string with a blessing that says it's okay. It's a subset of the string type that can contain a more limited set of values than the built-in string type.
For example, parsing a string into a Username, I'm asserting things about the string (e.g., it's <10 characters long, it contains only a-z0-9). If I just use the string type, that's not an accurate representation of what's legal for a Username because the string type implies any legal string is a valid value.
Re: How I write HTTP services in Go after 13 years
#143Earlier quoted context omitted.
The countless bugs I've had to deal with and all the time I've lost fixing these bugs caused by people who forgot to validate data in a certain place or didn't realize they had to do so proves to me that the overhead of calling a get on a wrapper type is totally worth it. I value the hours wasted on diagnosing a bug far more than the extra keystrokes and couple of seconds required to avoid it in the first place.
No, you’ve achieved an illusion of that as now your spending hours wasted on discovering where a developer forgot to call NewUsername and instead called Username{“broken”}. I cant see the value in this abstraction in Go.
In my opinion, this pattern breaks when the validation must return an error and everything becomes very verbose.
Re: How I write HTTP services in Go after 13 years
#144Earlier quoted context omitted.
> Then the zero value is their problem, not yours. Except for all those times you're the consumer of someone else's library and there's no way for them to indicate that creating a zero-valued struct is a bug. Again, it's the philosophy of "Just do the right thing everywhere and you don’t have to worry!" Sometimes it's nice to work with a type system where designers of libraries can actually prevent you from writing b…
> Except for all those times you're the consumer of someone else's library and there's no way for them to indicate that creating a zero-valued struct is a bug. Nonsense. Go has a built-in facility for documentation to communicate these things to other developers. Idiomatic Go strongly encourages you to use it. Consumers of the libraries expect it. > Sometimes it's nice to work with a type system where designers of li…
…where, "what GP seeks" is…
> way for [library authors] to indicate that creating a zero-valued struct is a bug
I'd say that's a really low and practical bar, you really don't need Coq for that. Good old Python is enough, even without linters and type hints.
Of course it's very easy to create an equivalent of zero struct (object without __init__ called), but do you think it's possible to do it while not noticing that you are doing something unusual?
Re: How I write HTTP services in Go after 13 years
#145Earlier quoted context omitted.
I've also seen it called primitive obsession, which is also applicable to other primitive types like using an integer in situations where an enum would be better.
Sadly enums are too advanced of a concept to be included in Go.
Re: How I write HTTP services in Go after 13 years
#146Earlier quoted context omitted.
Parse don't validate means that if you want a function that converts an IP address string to a struct IpAddress{ address: string } you don't validate that the input string is a valid IP address then return a struct with that string inside. Instead you parse that IP into raw integers, then join those back into an IP string. The idea is that your parsed representation and serializer are likely produce a much smaller an…
Thanks for that explanation! I hadn't appreciated that aspect of "parse, don't validate," before. But even with that understanding and from re-reading the post, that seems to be an extra safety measure rather than the essence of the idea. Going back to my original example of parsing a Username and verifying that it doesn't contain any illegal characters, how does a parser convert a string into a more direct represent…
Just for the sake of example, your internal representation might start from 0, and you just add 1 whenever you output it.
Your internal type might also not be a uint8. Eg in Python you would probably just use their default type for integers, which supports arbitrarily big numbers. (Not because you need arbitrarily big numbers, but just because that's the default.)
Re: How I write HTTP services in Go after 13 years
#147Earlier quoted context omitted.
No, you’ve achieved an illusion of that as now your spending hours wasted on discovering where a developer forgot to call NewUsername and instead called Username{“broken”}. I cant see the value in this abstraction in Go.
They can’t because value is not exported. They must use the NewUsername function, which forces the validation. In my opinion, this pattern breaks when the validation must return an error and everything becomes very verbose.
Re: How I write HTTP services in Go after 13 years
#148Earlier quoted context omitted.
The dual problem would be any function which forgets to accept a ParsedString instead of a string can skip parsing. Both cases appear to depend on there being a "checkpoint" all data must go through to cross over to the rest of the system, either at parsing or at UnvalidatedString construction.
> The dual problem would be any function which forgets to accept a ParsedString instead of a string can skip parsing. > Both cases appear to depend on there being a "checkpoint" all data must go through to cross over to the rest of the system, either at parsing or at UnvalidatedString construction. The difference is that if string is the trusted type, then it's easy to miss a spot and use the trusted string type for…
Re: How I write HTTP services in Go after 13 years
#149Earlier quoted context omitted.
This is a good design pattern, but be wary of doing validation too early. The design pattern allows you to do it as early or late as you like, but doesn't tell you when to do it. Often it's best to do it as part of parsing/validating some larger object. See Steven Witten's "I is for Intent" [1] for some ideas about the use of unvalidated data in a UI context. [1] https://acko.net/blog/i-is-for-intent/
I read through that piece and strongly disagree with the premise that their insight is somehow at odds with leaning into the type system for correctness. The legitimate insight that they have is that anchoring the state as close as possible to the user input is valuable—I think that that is a great insight with a lot of good applications. However, there's nothing that says you can't take that user-centric state and p…
Text has more possible states than the equivalent AST, many of which are useful when you haven't typed in all the code yet. Incomplete code usually doesn't parse.
This suggests that drafts should be represented as text, not an AST.
And maybe similarly for drafts of other things? Drafts will have some representation that follows some rules, but maybe they shouldn't have to follow all the rules. You may still want to save drafts and collaborate on them even though they break some rules.
In a system that's not an editor, though, maybe it makes sense to validate early. For a command-line utility, the editor is external, provided by the environment (a shell or the editor for a shell script) so you don't need to be concerned with that.
Re: How I write HTTP services in Go after 13 years
#150Earlier quoted context omitted.
There's a name for this anti-pattern: "Stringly typed"
I've also seen it called primitive obsession, which is also applicable to other primitive types like using an integer in situations where an enum would be better.
After playing with Rust, I changed my tune. The type system just forces you into the correct path, that a lot of code became boring because you no longer had to second guess what-if scenarios.