3 ms·
(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 artic
by 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.
- jmull 4y agoI don't know Haskell, but in the typescript code the types aren't doing anything... sendToEmail sends to any kind of Email. And if the code wants to know if an Email is verified, it inspects the verified field. > 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. Whether you centralize the verification code (or otherwise have a separation of concerns for it) or not is independent of whether you use the type system to express when an email is verified or some other mechanism.
- hither_shores 4y ago> sendToEmail sends to any kind of Email. That's my point, you don't need a combinatorial explosion of behaviors for every possible most-specific-type, you can just reuse existing ones. > And if the code wants to know if an Email is verified, it inspects the verified field. That's exactly what you shouldn't do. Runtime "type" inspection is just bad overly distributed parsing. The point of typing is to move errors to compile-time. emailLaunchCodes = (email: VerifiedEmail): Promise<void> => ... This doesn't need to inspect anything, because the type signature guarantees that someone else has already handled that. > Whether you centralize the verification code (or otherwise have a separation of concerns for it) or not is independent of whether you use the type system to express when an email is verified or some other mechanism. Separating the verification code - which is table stakes, really - doesn't guarantee that you're not accidentally adding or losing "verification" elsewhere. Types can and should.
- jmull 4y ago> The point of typing is to move errors to compile-time. You've got to understand: Whether or not a particular email address is verified or not isn't something that's generally known at compile time... That means compile time checks cannot actually guarantee if that email address is verified or not. The compiler cannot know something that isn't known. There must be runtime code somewhere doing the check. What people are really talking about with types here is that if you organize your code in a certain way, you can put the code that verifies whether an email address is verified or not in one place and use types to help make sure other code doesn't accidentally ignore or change that guarantee. That's good and fine. But... (1) you can do the very same thing without types; (2) Either mechanism is only as good as your code organization and controls that ensure there's a single place this is determined (and that that place is correct).