10 ms·
Strong Types and Their Impact on Testing
- lovelustus 12y agoBoomtown! This is awesome...
- TeeWEE 12y agoImho types should make the programmer live easier, not more difficult. Looking at the first code, and at the second peace of code. Imho the second piece of code is easier to read, but it does lack validation logic & testing. First of all, from a type-theory point of view, both Email and WebForm have the same type. (same signature). Also from a theoretical point of view, i think a type should not have an instance if its not valid and "EmailAddress" of value "aasda" should not exists. However it is allowed in his types. Also, I don't see how defining lots of very specific types such as FromAddress and ToAddress are really making live easier. Something like this does the job, and is typesafe (in golang): type EmailAddress string type Email struct { to EmailAddress from EmailAddress subject string body string } func sendEmail(webForm string) err = { email, err := validateEmailForm(form) # performs validation if err { return err } err := email_service.sendEmail(email) if err { return err } } Tests will simply use the returnValue of sendMail to validate its inner working.
- xwowsersx 12y agoGood point. Having a separate FromAddress and ToAddress was probably overkill. You're right about the WebForm and Email having the same type. The form submission could probably just produce an Email directly. I don't know much about go. What is "err"?
- icebraining 12y agoGo follows the C tradition of returning errors instead of using exceptions. 'err' is a variable of type "error"[1], which should either be nil or have an error value. [1] http://golang.org/pkg/builtin/#error http://golang.org/pkg/builtin/#error
- xwowsersx 12y agoso the return type of this little program is "error" and nil if no error?
- icebraining 12y agoYes. It's either a value that implements the 'error' interface or nil.
- badbath 12y agoThat's nuts. All of your code now has to check if err != nil, etc. Furthermore, the sendEmail and validateEmailForm values are essentially ignored and used only for their possible error values. Why should I wrote my program as if the thing I'm concerned primarily with is errors? Much better to encode the failure into the type. You could have, for example, some function that returns Either[Error, SendEmailAction] where SendEmailAction is a function that, when called, will perform the effect of sending an email. Now I can deal with the happy path by mapping on the right-biased Either.
- ecopoesis 12y agoNot if you use Scala. With Scala, you'd just map or for comprehension over the result.
- icebraining 12y agoThis has been discussed in pretty much every golang thread here on HN :)
- dllthomas 12y agoHaving a separate FromAddress and ToAddress serves a somewhat similar purpose to labeled arguments. You can't mix them up, and you are informed if they change.
- michaelochurch 12y agoStatic types. Python is strongly typed, although possibly not as much as we might hope. This fails: "cat" + 1 with a type error, but it happens at runtime. (Oddly enough, Scala permits it.) Python's type system isn't very expressive-- for example, you have "list" and "dict" types but not "list of int"-- and since you can branch without type congruence (because Python's "if" is a statement, not an expression) you can lose static knowledge pretty quickly, but Python does block type errors. It just does so at runtime, which means that the problem may be hard to reproduce and the error/failure distance can be high. I agree with most of what's said in the OP. I just want to push back against the use of "strong" and "static" typing as if they were interchangeable. I'm in the same boat-- generally an advocate for compile-time static typing-- but to conflate static/dynamic with strong/weak is to misrepresent the issue, and unfair to the dynamically-typed languages. All of the modern dynlangs (e.g. Python, Ruby, Clojure) enforce type safety. They just do it at runtime, whereas Haskell, Scala and Ocaml do it at compile time.
- xwowsersx 12y agoYou're right. I wasn't precise and conflated those issues. Thank you.
- michaelochurch 12y agoYou are far from the first. Many, many years ago, I made a more embarrassing mistake. I insisted in a job interview that OCaml was dynamically typed because it didn't require explicit typing. (To my naive eye, it "looked" like a dynlang). When I realized that I was interviewing with an expert, I backed off and admitted that I was wrong, not entirely sure why but not about to argue with someone who knew the language in depth. (And, of course, I was wrong.) I still got the job. I'd actually argue that strong vs. weak is a continuum and somewhat subjective. One of my gripes with Scala is that the type system (as used in the field) isn't strong enough. It will implicitly auto-promote types in ways you might not want (i.e. List[Int] :+ String = List[Any]). It can be a strong type system, but the quality of programmers has a huge influence on your experience with Scala. If you have bad programmers who do things the Enterprise Java Way, you can quickly end up with compile-speed issues. But that's another rant. Scala's great for mid-sized projects or when the developers are strong.
- Offler 12y ago3. By using meaningful values and separating side-effecting functions from pure ones, we can more easily reason about our program I'm not sure how that's something that static typing provides, just seems like good functional programming/referential transparency.
- sgrove 12y agoTrue, but signatures help statically verify that side-effects can't happen at a call site, giving you reassurances that it's non-side-effecting. Same with referential transparency, I believe.
- aharris88 12y agoWhat is this written in reply to?
- xwowsersx 12y agoIn part, this gist: https://gist.github.com/darrencauthon/0c11c7fd24f82733bd12 https://gist.github.com/darrencauthon/0c11c7fd24f82733bd12
- darrencauthon 12y agoYes, my gist, but also a Twitter conversation on how strong typing altered the need for tests. I've heard from a couple Haskell programmers about how the language practically eliminated the need for tests. I don't see how that's possible, but I also know Haskell. I asked for a specific code example, and I threw out the scenario of a client who wants a contact us form that logs all responses and sends notification emails. I threw out a simple Ruby example where I tested the functionality, and I'm waiting to see the alternative.
- teacup50 12y agoAre you waiting to see the alternative, or waiting to be right? The example given seems clear enough to extrapolate from.
- darrencauthon 12y agoI'm waiting to see the alternative. It's an honest question to ask in the face of an assertion about testing -- or how strong or static typing eliminates the need to verify that the code behaves as written. In the sample I provided, I provided a concrete example as to how I could verify that the code accomplishes the task. If tests are not needed, then fine... show me how with code that accomplishes the task and that show how. Like the "unit" where the two operations are covered in one method... Sure, but how else would it be done? The client asked for the log and the email, so somehow, someway... someone has to write code to do it. I threw up my Ruby sample in just a few minutes... why can't the alternative be coded up in a few more?
- serve_yay 12y ago> we can easily shoot ourselves in the foot. We might mistakenly write Email("hello", "jason", "jason@gmail.com", "body of email"), for example. I would just solve that with keyword arguments. Or, in Javascript, have the constructor take an object whose keys are fromAddress, toAddress, etc. (Pretty much the JS/ES5 version of keyword args.)
- xwowsersx 12y agoAnd then if you're missing a key in your object, your program blows up at runtime.
- serve_yay 12y agoIn JS, true. You can't do any of this stuff in JS! :) But I guess my real point was that that seemed like more of an argument against positional parameters than for lots of typing. I like how ObjC handles this, I wish more languages did it that way.
- xwowsersx 12y agoInspired by this Twitter convo, for those who care: https://twitter.com/LeviNotik/status/534190509947097089 https://twitter.com/LeviNotik/status/534190509947097089
- marcelk 12y agoInteresting article, especially encoding as much domain knowledge as possible into types by encapsulating simple strings/ints/etc into case classes. Fully agree :) However, performing validation outside of the model makes it possible to create a model with invalid data. For example, calling val x = EmailAddress("@") would pass. Thus, a developer must remember to call the validation every time they create such a case class. In my experience, this wouldn't hold since developers are as lazy as the code allows them to be. Instead, why not include requirements directly into a case class constructor. For example: case class EmailAddress(addr: String) { private val emailRegexp = ... require(emailRegex.findFirstIn(addr).isDefined, s"Email $addr not valid") } In this way, one can be sure that an EmailAddress case class always holds a correct email and can always safely rely on this assumption. Also, one can easily test (e.g. with ScalaCheck) that the constructor throws an IllegalArgumentExceptin. The only downside is the conversion of these exceptions into Either, Scalaz Disjunctions or Scalaz Validation. Of course, it's not that pretty and idiomatic to convert try-catch blocks into Either. But IMHO, it's a small cost in exchange for the guarantee that if your model exists it must be valid.
- jimbokun 12y agoWhat's with functional languages and the obsession with naming things after random sequences of punctuation characters? validateFrom(form.from).toValidationNel |@| validateTo(form.to).toValidationNel |@| validateBody(form.body).toValidationNel |@| validateRecipient(form.recipient).toValidationNel)(Email.apply) What's the difference between "|@|" or "@" or "|@" or "|@" or "@@" or "@@@" or... I'm sure this made perfect sense to someone at the time this decision was made, but naming things this way does absolutely nothing to help someone reading the code or using your library for the first time. Can anyone explain the motivation for naming things in this way?
- xwowsersx 12y agoBecause they represent a well-known concept in category theory and symbols are a simple, uniform way of representing that concept. It's terminology. Math has terminology and so does category theory. Should we write (3 multiplied by 2 added to 5) in math? No. We write 3 * 2 + 5. Once you know those symbols, they're well understood and concise.