4 ms·
Why is it better to have two email types, VerifiedEmail and UnverifiedEmail vs. one Email type with an "isVerified" field? One type is probably going to align
by jmull 4y ago
Why is it better to have two email types, VerifiedEmail and UnverifiedEmail vs. one Email type with an "isVerified" field?
One type is probably going to align with storage and transport better, and you probably mostly want to treat verified and unverified email addresses the same except for some very specific situations. (E.g., maybe only your EmailBlaster cares, where it's like a privilege: some can send to unverified emails and some can't.)
This seems like a bad for types to me.
(It's all code you write and data you design -- types are just one tool... you need to think about they best tool, not get fixated on one, no matter nice it is.)
- blandflakes 4y agoI think you understood the use, but value the safety less than I do: > maybe only your EmailBlaster cares, where it's like a privilege: some can send to unverified emails and some can't A boolean flag is strictly inferior, because it is a runtime check. You can only ever be sure that you're processing verified emails at runtime, and there's no way to require that the code guards against that in all places. If they're different types, you can't even pass an unverified email to code that needs verified emails, so you eliminate the entire possibility at compile-time.
- throway232lasdf 4y ago> Why is it better to have two email types, VerifiedEmail and UnverifiedEmail vs. one Email type with an "isVerified" field? You obviously have no idea what a type is. type VerifiedEmail = { email: string; is_verified: true; }; type UnverifiedEmail = { email: string; is_verified: false; };
- Smaug123 4y agoIn fairness this takes an unusually strong type system to express, doesn't it? Typescript can do it, but I don't think e.g. Haskell98 can do it out of the box in an analogous way? (Of course, it's hard to prove a negative and I'm not super familiar with Haskell, but my evidence is that I'm pretty certain F# can't.)
- hither_shores 4y ago> but I don't think e.g. Haskell98 can do it out of the box in an analogous way? This is basically the runtime representation of `data Email = Verified { email :: string } | Unverified { email :: string }`, but promoting `Verified` and `Unverified` to type level requires an extension: data Email (verified :: bool) where Verified :: string -> Email true Unverified :: string -> Email false
- jmull 4y agoThis is expressing the same value in two different ways. That's worse, not better.
- greymalik 4y agoType correctness can be verified at compile time while Boolean values can’t.
- kajaktum 4y agoIt's so that when you are 50 function calls deep you don't have to remember if you are handling a verified email or not. This is the same problem I have with "sum types" as implemented in languages that don't have algebraic data types. You either have a massive struct that contains mostly nullable values or something like a tagged union. >verified and unverified email addresses the same except for some very specific situations. This is when typeclasses are a useful concept (or interfaces). Instead of designing your function around a concrete type, you can codify that the caller needs to provide any types that satisfy some requirements. For example, if the function only cares that the input can be treated as a string, you can ask for something like `As<String>`. Then, the caller can provide literally anything as long it implements `As<String>`. > types are just one tool Indeed, types are just a tool. But it is a much better tool. Its an electronic shaver instead of rusty axe. This is my biggest gripe with "simple and small languages". More often than not, you just end up writing more verbose and complicated code just to compensate. To illustrate, I wanted to implement `INCR KEY VALUE` from Redis. ``` item, exists := db.keys[key] if !exists { return } value, ok := item.Value().(string) if !ok { return } intValue, err := strconv.ParseInt(value, 10, 64) if err != nil { return } intValue++ // <== this is literally the only meaningful work I am doing db.keys[key] = intValue; ```
- tel 4y agoIt's because you can write these types: recognize : String -> UnverifiedEmail validate : UnverifiedEmail -> VerifiedEmail send : (VerifiedEmail, Message) -> () You can then use visibility controls to universally guarantee that recognize and validate must be called before send. No test can ensure this is true. Under the presumption that send should only perform work on verified emails, the alternative is not being able to be confident that emails passed to send are pre-verified. This means that send must check this flag, and therefore have the ability to fail due to non-verification. This isn't inherently a problem, but it can lead to a failure to separate concerns. If one part of your system is responsible for parsing and validation and a separate part responsible for interacting with the sending machinery, it's unfortunate if the latter part can fail due to a failure to verify the email. These systems have now implicitly shared responsibility. You can try to guarantee that no email is passed from the first system to the second without being verified, but this can be challenging. It's a universal property. Tests can show the presence but not the absence of bugs. But those types we showed at the beginning provide exactly that guarantee.
- gilbetron 4y agoBut then you can just have a "makeEmailVerified" that takes an UnverifiedEmail and converts it to a VerifiedEmail without verification. Then the "send" function still needs to check things.
- tel 4y agoTypically, the owner of the VerifiedEmail type restricts your ability to construct new values of that type by making the constructor private and then only "blessing" a small number of public constructors, each one performing that verification. If any type could be converted to any other (compatible) type for any reason whatsoever then types would have no semantic value. But fortunately, we can control how types are constructed and used.
- jmull 4y ago(Replying to my own post) Wow, lots of great, thoughtful replies, thanks! But I'm not convinced... Maybe it's just a bad example (this is what the main article is about, though)... Generally speaking, you need to be able to send emails to both verified and unverified emails. The difference is in what the email you are sending is about. That's why VerifiedEmail as a type doesn't make a lot of sense to me. You'll need sendToUnverifiedEmail(email: UnverifiedEmail) and sendToVerifiedEmail(email: VerifiedEmail), and have code to get the right type to pass to the right function the in the right circumstance... You've got the same potential for getting this stuff wrong, whether you express it in a type or in imperative code or however you express it. Replies are generally assuming the type part of the code is bug-free and the imperative part of the code is not, which just isn't reasonable. Also, the static vs. runtime stuff is irrelevant to this example: the verified status of an email address is a runtime property that cannot be known at compile time (OK, I'm assuming you don't hard-code verified email addresses). I.e., due to a bug, a value of type VerifiedEmail could be created for an email address that is not really verified. Then, your static checks for VerifiedEmail don't help you at all. Further, "verified" vs. "unverified" is really a business concern around when it's OK to send certain kinds of emails to the address. It has a fairly standard definition, but there are qualifiers for email addresses that are at least as important to a business that aren't. E.g, did the owner of the email address opt in to marketing emails? Or opt out? Or did not express a preference (yet)? Are you going to have types for those? You'd have to have 3 X 2 types to encode that... that's (verified, unverified) X (opted-in, opted-out, didn't specify) types. Then your business adds a new kind of email, securityAlerts, so now you've got (verified, unverified) X (opted-in, opted-out, didn't specify) X (gets-security-alerts, doesnt-get-security-alerts). Oh wait, some emails shouldn't go to banned people. So now you've got: (verified, unverified) X (opted-in, opted-out, didn't specify) X (gets-security-alerts, doesnt-get-security-alerts) X (banned, not-banned). And you need a "send" for each type, called at the right spot. So... Are the types really helping here?
- hither_shores 4y ago> You'll need sendToUnverifiedEmail(email: UnverifiedEmail) and sendToVerifiedEmail(email: VerifiedEmail), and have code to get the right type to pass to the right function the in the right circumstance... Only if you're using a language with an insufficiently strong type system (e.g. Java, C#) in typescript: type UnverifiedEmail = { address: string, verified; false } type VerifiedEmail = { address: string, verified; true } ... type Email = UnverifiedEmail | VerifiedEmail | FooBarBazEmail const sendToEmail = (email: Email): Promise<void> = ... in Haskell: class SendTo t where sendTo :: t -> IO () newtype Email = Email string instance SendTo Email where sendTo (Email address) = ... newtype VerifiedEmail = VerifiedEmail Email deriving (SendTo) newtype FooBarBazEmail = FooBarBazEmail Email deriving (SendTo) > I.e., due to a bug, a value of type VerifiedEmail could be created for an email address that is not really verified. Then, your static checks for VerifiedEmail don't help you at all. Of course they do - they tell you that the bug is in the verification code, and not in any of the thousands of lines of business logic separating it from the place where the error was found.