4 ms·
I think you've really missed the point.
by noelwelsh 2y ago
I think you've really missed the point.
- bvrmn 2y agoI'm curious how would look like a real example with different Email classes. It seems silly. You don't often need a VerifiedEmail. But VerifiedUser is more useful from VerifiedEmail. You could get an implicitly verified email without introducing a new type.
- TeMPOraL 2y agoBut you don't want "implicitly verified", when there exist any code to which the difference between "verified" and "not verified" matters in some way. In your own example, if you happen to use e-mail verification as proxy for user verification, then the very code that creates VerifiedUser instances would want to have VerifiedEmail as input!
- bvrmn 2y agoYou don't need VerifiedUser in the moment user click verification link. It's a state changing action. You get verified user from db and work with it later.
- TeMPOraL 2y agoFine. It still has merit that the code pulling verified user data from verified users table returns VerifiedUser objects, so the code processing them is clear about its assumptions, and won't accidentally get reused or refactored in UnverifiedUser flows.
- noelwelsh 2y agoAbstracting the email example, if you have a finite state machine that verifies some fact, you can model each state as a type. Their example goes Email -> ValidEmail -> VerifiedEmail. You could equally go String -> Email -> VerifiedEmail, where String is what the user submits as their email address. If you are verifying emails in the context of verifying users you could go Map<String, String> -> User -> VerifiedUser, where Map<String, String> is what you receive from an HTML form submission. Getting hung up on the details of which type you use is missing the overall point.
- atoav 2y agoGood point, the names really don't matter and the first type could as well have been a string. I just wanted to illustrate the concept of using the type system to add important information and guarantees to certain variables. Your state machine analogy is a good one, because the conversion between different types is a bit like the transitions between states: you have to make explicit how exactly they are meant to happen (or whether they are actually possible). This is a good thing, having less degrees of freedom may seem like it makes it harder to code, but in fact it allows you to reason better about what the system is doing at any given point in your code.
- bvrmn 2y agoState machine is always a trivial part. Email verification is stupidly simple task. But User propagation through the system and available actions based on verified email are not.
- codetrotter 2y ago> You don't often need a VerifiedEmail. But VerifiedUser is more useful from VerifiedEmail. You could get an implicitly verified email without introducing a new type. Be careful about how the email is implicitly verified though, and how different login methods and assumptions interact when for example migrating users from one system or platform to another. https://krebsonsecurity.com/2024/07/researchers-weak-security-defaults-enabled-squarespace-domains-hijacks/ https://krebsonsecurity.com/2024/07/researchers-weak-securit... > analysis released by security experts at Metamask and Paradigm finds the most likely explanation for what happened is that Squarespace assumed all users migrating from Google Domains would select the social login options — such “Continue with Google” or “Continue with Apple” — as opposed to the “Continue with email” choice > Squarespace never accounted for the possibility that a threat actor might sign up for an account using an email associated with a recently-migrated domain before the legitimate email holder created the account themselves > since there’s no password on the account, it just shoots them to the ‘create password for your new account’ flow. And since the account is half-initialized on the backend, they now have access to the domain in question This kind of mistake makes me think that it’s better to be super explicit about the state of everything when it comes to accounts. So even if it may seem excessive to have VerifiedEmail as a type instead of marking the account as verified or not. I will prefer being explicit. And in medium to big size systems with multiple pieces of user account data that require separately keeping track of verification state it will be necessary anyway. For example even something as simple as having one email and one phone number associated with a user. Or beyond that one user having multiple email addresses or multiple phone numbers etc.
- atoav 2y agoThank you for beinging up this excellent display of hidden dangers to my very crude example about why type systems should be used if they are there. My point was specifically about complexity. Using a string for all kind of emails of course works, tying guarantees to other things like VerifiedUser works as well. These implicit guarantees just fail to safe your ass once things get more complex and there is an edge case you didn't think of.
- bvrmn 2y agoSo in the end users were in state "not verified" after migration. Seems email is a secondary property.
- mrkeen 2y agoI regret shooting from the hip, and answering the comment directly rather than tying it back to the article. String->(valid)Email is 'Parse', (invalid)Email->ValidEmail is 'Validate'. > That way you never have to guess whether the email adress is unchecked Now you just get to guess whether, for every class, there is actually a ValidClass that you were supposed to be using, instead of Class. authenticate(User user); ^ This is a bug. We just let a malicious User object in because we invested extra time, effort and sloc into making both a User and a ValidUser class, disregarding the first "simple idea" from the article: >> 1. Use a data structure that makes illegal states unrepresentable Or as I wrote: > only make classes for valid objects
- noelwelsh 2y ago> Now you just get to guess whether, for every class, there is actually a ValidClass that you were supposed to be using, instead of Class. The type system stops this. You can't pass, e.g. a User to authenticate because it only accepts a ValidUser. > only make classes for valid objects Using primitives (String, Int, etc.) for unvalidated input, and custom classes for validated input is fine in many cases. However, sometimes you need to represent data that is in the process of being validated (e.g. when validation takes time, like waiting for a user to validate their email address) and then these intermediate classes arrive.
- szundi 2y agoBut your colleage just implemented it with User - because he could
- noelwelsh 2y agoNo tool can fully protect against user incompetence (pun intended). You can have the fanciest type system in the world and still implement everything as a String, for example.
- fiddlerwoaroof 2y agoI think, for most cases with partial validation, you should parse the fields first into valid types for those fields and store them in a hashmap or something. When all the fields have been collected, parse the hashmap into your aggregate type.