Strong Types and Their Impact on Testing
levinotik.com
levinotik.com
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.I don't know much about go. What is "err"?
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.
Golang is a really simple language, but all the simple concepts, and the lack of OOP, forces you to think more about your code. Also you are forced to think about error conditions.
I'm still playing around with it. So i dont have a final opinion on go.
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.
I'm not sure how that's something that static typing provides, just seems like good functional programming/referential transparency.
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.)
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.
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?
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.
The example given seems clear enough to extrapolate from.
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?
Haskell's type system does statically check many things you might want to write tests for which makes those particular tests unnecessary, but it doesn't eliminate the need for tests in general. If you hear any Haskell programmers making claims that sound like that please ask them to be more precise.
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.
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.
Wow, that seems to totally miss the point of using types to help you catch errors.
There have been attempts to fix this - WartRemover specifically has a Wart for any uses of Any for example.
Given that the language supports subtype polymorphism, this is exactly how the type inference should work. Any is the narrowest available supertype.
If you don't like it, there's a pretty simple fix: add an explicit type ascription, which you should be doing for the return values of any methods anyway.
The distinction between compile time failure and runtime failure is an important one, but not the only thing going on.
printf("%d", 42.0);
Strong typing means the type of each chunk of memory is known and can't be arbitrarily changed, and then static/dynamic refers to how much type information is present at compile time.Might be a losing battle, though. Strong/weak has been overloaded in this way for a long time.
> printf("%d", 42.0);
which does not demonstrate your point and is valid Python:
>>> "%d" % 42.0
'42'
also the more odd >>> "%.2f" % True
'1.00'
and who could forget >>> "foo" * 3
'foofoofoo'
edit: forget about this, for some reason I'd forgotten what the example would output…Yeah I kinda forgot about the result.
> Python doesn't even have a printf function, where did you get the idea it was supposed to be Python?
Nowhere? I never assumed your example was python, I was showing something irrelevant.
If that's not what you mean, that was not at all evident.
It's not, I pretty much fell into the issue you decry at the end of your comment: I considered the operation being allowed at all (by the compiler and the runtime) not being evidence of weak typing. Which I guess is technically correct, but is mostly completely irrelevant to the point you were making.
True being coercible to a number is unfortunate, but it's still not the same as what C does with that printf.
"foo" * 3 is just an example of a polymorphic operator. You could implement the same in any strongly typed language that allows polymorphic operators.
If you tried it with variables you would get a type error:
> let x = 1.0 :: Double
> let y = 1 :: Int
x + y
<interactive>:10:5:
Couldn't match expected type ‘Double’ with actual type ‘Int’
In the second argument of ‘(+)’, namely ‘y’
In the expression: x + y
It sometimes looks like conversion because the literal `1` is polymorphic and can have a more specific type like `Int` or `Double` inferred from context. λ> :t 5
5 :: Num a => a
λ> :t 5.6
5.6 :: Fractional a => a
λ> :t (5 :: Integer)
(5 :: Integer) :: Integer
The type of the literal "Num a => a" means that the literal "5" works for any type in the Num typeclass. So you can mix literals with any numeric type because the compiler will correctly infer the right type. You can even get usages like this: λ> :t (\x -> x + 5)
(\x -> x + 5) :: Num a => a -> a
λ> let f = (\x -> x + 5) in (f (0 :: Int), f (0 :: Double))
(5,5.0)If we want to be really pedantic, in Python 2.X, True isn't coercible to an integer, it is an integer.
>>> True == 1 True >>> isinstance(True, int) True
This is an artifact of the fact that when Python was first created, it didn't have boolean types, so when they were added later, "True" and "False" were bolted on as aliases to the integers 1 and 0.
C#, for example, is mostly strong and static. But within 'unsafe' contexts you're allowed a lot of those kinds of C-style shenanigans, and you can replicate C unions using the StructLayout and FieldOffset attributes, so the type system is ultimately optional.
I think of weak typing as a whole continuum of different things. At an extreme end you have C's level of optional typing. But there's also more tame stuff like implicit casting (which C# also has) that I'd also count as a form of weak typing.
C doesn't just let you reinterpret bits at will. You have to jump through one of a few very specific hoops. You have to cast, you have to use a union, or you have to use a vararg function. (Granted, those are some pretty big hoops, but my point is, you have to use one in order to reinterpret bits.)
I don't think C's type promotion rules are in the same category. You can promote a short to a double when adding it to a double, but that's not exactly "reinterpreting bits at will".
I agree that type promotion isn't the same, but I didn't mention that anyway....
Sure you can.
>> from array import array
>> a = array('i')
>> a.append(3)
>> a
array('i', [3])
>> a.append('Hello')
TypeError: an integer is required
The default "list" type just happens to be an heterogeneous list.Well, a lot of them. Some are weakly typed, and IMO weak typing is genuinely damaging whereas I'll concede some room for the static/dynamic debate.
Example: What's "1" + 1?
Answer: In JavaScript, it's "11". In PHP, it's 2. Meaning it's possible that it could mean two different things in the same file.
If you're spanning languages, that's not unusual. I have Haskell code with C in quasiquotes. What's x /= 2?
You could have the same issue with languages with indisputably strong, static typing with different semantics if there was a way of embedding them in the same file.
As _yosefk said, the interpreter can assume a more compact representation.